Add manifest URL field, reset keys when manifest changes, organize repo, update to Manifest v3 #3
							
								
								
									
										30
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
pnpm-debug.log*
 | 
			
		||||
lerna-debug.log*
 | 
			
		||||
 | 
			
		||||
node_modules
 | 
			
		||||
dist
 | 
			
		||||
dist-ssr
 | 
			
		||||
*.local
 | 
			
		||||
 | 
			
		||||
frontend/node_modules
 | 
			
		||||
frontend/dist
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
.idea
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
 | 
			
		||||
# extension release folder
 | 
			
		||||
extension-release
 | 
			
		||||
							
								
								
									
										3
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
react/
 | 
			
		||||
frontend/dist/
 | 
			
		||||
frontend/src/assets/
 | 
			
		||||
							
								
								
									
										8
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
    "trailingComma": "es5",
 | 
			
		||||
    "tabWidth": 4,
 | 
			
		||||
    "semi": true,
 | 
			
		||||
    "singleQuote": false,
 | 
			
		||||
    "useTabs": false,
 | 
			
		||||
    "printWidth": 100
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
# CDRM Extension
 | 
			
		||||
 | 
			
		||||
An extension to show keys from DRM protected content, which are used to decrypt content.
 | 
			
		||||
 | 
			
		||||
## Notes
 | 
			
		||||
 | 
			
		||||
Keep these extension core files inside `src`:
 | 
			
		||||
 | 
			
		||||
- `background.js`
 | 
			
		||||
- `content.js`
 | 
			
		||||
- `inject.js`
 | 
			
		||||
- `manifest.json`
 | 
			
		||||
 | 
			
		||||
The `mv2` folder is for Manifest v2 backup for legacy reasons.
 | 
			
		||||
 | 
			
		||||
Frontend React source stays in `frontend`.
 | 
			
		||||
 | 
			
		||||
The build process will take care of everything into `extension-release`.
 | 
			
		||||
 | 
			
		||||
To update the version across the entire project, simply change the version number in the root `package.json`. The build script will handle version sync automatically to both the extension's version and the frontend's title bar.
 | 
			
		||||
 | 
			
		||||
## Build instructions
 | 
			
		||||
 | 
			
		||||
### Prerequisites
 | 
			
		||||
 | 
			
		||||
- Node.js v21 or higher. [Download Node.js here](https://nodejs.org/en/download).
 | 
			
		||||
 | 
			
		||||
### How to build by yourself
 | 
			
		||||
 | 
			
		||||
- Open terminal at the project root
 | 
			
		||||
 | 
			
		||||
- Run the build script:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
npm install
 | 
			
		||||
npm run buildext
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This will:
 | 
			
		||||
 | 
			
		||||
- Sync the version number from the root `package.json` to `src/manifest.json` and `frontend/package.json`
 | 
			
		||||
- Install frontend dependencies if needed
 | 
			
		||||
- Build the React frontend
 | 
			
		||||
- Clean and prepare the `extension-release` folder
 | 
			
		||||
- Copy extension files in `src`, built frontend assets, and icons into `extension-release`
 | 
			
		||||
 | 
			
		||||
### How to load the extension in Google Chrome or Chromium browsers
 | 
			
		||||
 | 
			
		||||
1. Go to `chrome://extensions/`
 | 
			
		||||
2. Enable **Developer mode**
 | 
			
		||||
3. Click **Load unpacked** and select the `extension-release` folder
 | 
			
		||||
4. Verify the extension is working by clicking its icon or opening the developer console (F12) to check for any logs or errors.
 | 
			
		||||
@ -1,84 +0,0 @@
 | 
			
		||||
// Open popout window when the extension icon is clicked
 | 
			
		||||
chrome.browserAction.onClicked.addListener(() => {
 | 
			
		||||
  chrome.windows.create({
 | 
			
		||||
    url: chrome.runtime.getURL("react/index.html"),
 | 
			
		||||
    type: "popup",  // opens as a floating window
 | 
			
		||||
    width: 800,
 | 
			
		||||
    height: 600
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Listen for messages and store data in chrome.storage.local
 | 
			
		||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
 | 
			
		||||
  const { type, data } = message;
 | 
			
		||||
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case "DRM_TYPE":
 | 
			
		||||
      console.log("DRM Type:", data);
 | 
			
		||||
      chrome.storage.local.set({ drmType: data });
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case "PSSH_DATA":
 | 
			
		||||
      console.log("Storing PSSH:", data);
 | 
			
		||||
      chrome.storage.local.set({ latestPSSH: data });
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case "KEYS_DATA":
 | 
			
		||||
      console.log("Storing Decryption Keys:", data);
 | 
			
		||||
      chrome.storage.local.set({ latestKeys: data });
 | 
			
		||||
      break;
 | 
			
		||||
    
 | 
			
		||||
    case "LICENSE_URL":
 | 
			
		||||
      console.log("Storling License URL " + data);
 | 
			
		||||
      chrome.storage.local.set({licenseURL: data});
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      console.warn("Unknown message type received:", type);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Set initial config and injection type on install
 | 
			
		||||
chrome.runtime.onInstalled.addListener((details) => {
 | 
			
		||||
  if (details.reason === "install") {
 | 
			
		||||
    chrome.storage.local.set({ valid_config: false }, () => {
 | 
			
		||||
      if (chrome.runtime.lastError) {
 | 
			
		||||
        console.error("Error setting valid_config:", chrome.runtime.lastError);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("valid_config set to false on first install.");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
 | 
			
		||||
      if (chrome.runtime.lastError) {
 | 
			
		||||
        console.error("Error setting Injection Type:", chrome.runtime.lastError);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("Injection type set to LICENSE on first install.");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    chrome.storage.local.set({ drm_override: "DISABLED" }, () => {
 | 
			
		||||
      if (chrome.runtime.lastError) {
 | 
			
		||||
        console.error("Error setting DRM Override type:", chrome.runtime.lastError);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("DRM Override type set to DISABLED on first install.");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    chrome.storage.local.set({ cdrm_instance: null }, () => {
 | 
			
		||||
      if (chrome.runtime.lastError) {
 | 
			
		||||
        console.error("Error setting CDRM instance:", chrome.runtime.lastError);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("CDRM instance set to null.");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    chrome.storage.local.set({ cdrm_api_key: null }, () => {
 | 
			
		||||
      if (chrome.runtime.lastError) {
 | 
			
		||||
        console.error("Error setting CDRM API Key:", chrome.runtime.lastError);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log("CDRM API Key set.");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										119
									
								
								buildext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								buildext.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
			
		||||
import { execSync } from "child_process";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import { minify } from "terser";
 | 
			
		||||
import url from "url";
 | 
			
		||||
import syncVersion from "./syncVersion.js";
 | 
			
		||||
 | 
			
		||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
 | 
			
		||||
const frontendDir = path.join(__dirname, "frontend");
 | 
			
		||||
const distDir = path.join(frontendDir, "dist");
 | 
			
		||||
const srcDir = path.join(__dirname, "src");
 | 
			
		||||
const iconDir = path.join(__dirname, "icons");
 | 
			
		||||
const releaseDir = path.join(__dirname, "extension-release");
 | 
			
		||||
 | 
			
		||||
const run = (cmd, cwd) => {
 | 
			
		||||
    console.log(`🛠️ Running: ${cmd}`);
 | 
			
		||||
    execSync(cmd, { cwd, stdio: "inherit" });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const copyDir = async (src, dest) => {
 | 
			
		||||
    await fs.promises.mkdir(dest, { recursive: true });
 | 
			
		||||
    await fs.promises.cp(src, dest, {
 | 
			
		||||
        recursive: true,
 | 
			
		||||
        force: true,
 | 
			
		||||
        filter: (src) => !src.endsWith(".map"),
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const minifyJS = async (jsContent) => {
 | 
			
		||||
    try {
 | 
			
		||||
        const result = await minify(jsContent, {
 | 
			
		||||
            compress: {
 | 
			
		||||
                drop_console: false, // Keep console logs for debugging
 | 
			
		||||
                drop_debugger: true,
 | 
			
		||||
                pure_funcs: ["console.debug"],
 | 
			
		||||
            },
 | 
			
		||||
            mangle: {
 | 
			
		||||
                reserved: ["chrome"], // Don't mangle chrome API
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
        return result.code;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.warn("⚠️ Minification failed, using original:", error.message);
 | 
			
		||||
        return jsContent;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Copy and minify JavaScript files from src directory
 | 
			
		||||
const copyAndMinifySrcFiles = async (src, dest) => {
 | 
			
		||||
    await fs.promises.mkdir(dest, { recursive: true });
 | 
			
		||||
 | 
			
		||||
    const entries = await fs.promises.readdir(src, { withFileTypes: true });
 | 
			
		||||
 | 
			
		||||
    for (const entry of entries) {
 | 
			
		||||
        const srcPath = path.join(src, entry.name);
 | 
			
		||||
        const destPath = path.join(dest, entry.name);
 | 
			
		||||
 | 
			
		||||
        if (entry.isDirectory()) {
 | 
			
		||||
            await copyAndMinifySrcFiles(srcPath, destPath);
 | 
			
		||||
        } else if (entry.name.endsWith(".js")) {
 | 
			
		||||
            // Minify JavaScript files
 | 
			
		||||
            console.log(`🗜️ Minifying ${entry.name}...`);
 | 
			
		||||
            const content = await fs.promises.readFile(srcPath, "utf8");
 | 
			
		||||
            const originalSize = Buffer.byteLength(content, "utf8");
 | 
			
		||||
            const minified = await minifyJS(content, entry.name);
 | 
			
		||||
            const minifiedSize = Buffer.byteLength(minified, "utf8");
 | 
			
		||||
            const savings = (((originalSize - minifiedSize) / originalSize) * 100).toFixed(1);
 | 
			
		||||
            console.log(
 | 
			
		||||
                `   📊 ${entry.name}: ${originalSize} → ${minifiedSize} bytes (${savings}% smaller)`
 | 
			
		||||
            );
 | 
			
		||||
            await fs.promises.writeFile(destPath, minified, "utf8");
 | 
			
		||||
        } else {
 | 
			
		||||
            // Copy other files as-is
 | 
			
		||||
            await fs.promises.copyFile(srcPath, destPath);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const main = async () => {
 | 
			
		||||
    await syncVersion();
 | 
			
		||||
 | 
			
		||||
    console.log("🚀 Starting extension build...");
 | 
			
		||||
 | 
			
		||||
    // 1. Install frontend deps if needed
 | 
			
		||||
    if (!fs.existsSync(path.join(frontendDir, "node_modules"))) {
 | 
			
		||||
        console.log("📦 node_modules not found. Running npm install...");
 | 
			
		||||
        run("npm install", frontendDir);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 2. Build frontend
 | 
			
		||||
    console.log("📦 Building frontend...");
 | 
			
		||||
    run("npm run build", frontendDir);
 | 
			
		||||
 | 
			
		||||
    // 3. Clean release folder
 | 
			
		||||
    if (fs.existsSync(releaseDir)) {
 | 
			
		||||
        console.log("🧹 Cleaning existing extension-release folder...");
 | 
			
		||||
        await fs.promises.rm(releaseDir, { recursive: true, force: true });
 | 
			
		||||
    }
 | 
			
		||||
    await fs.promises.mkdir(releaseDir);
 | 
			
		||||
 | 
			
		||||
    // 4. Copy and minify src files
 | 
			
		||||
    console.log("📦 Copying and minifying src files...");
 | 
			
		||||
    await copyAndMinifySrcFiles(srcDir, releaseDir);
 | 
			
		||||
 | 
			
		||||
    // 5. Copy frontend dist files to release (merged at root)
 | 
			
		||||
    console.log("📦 Copying frontend dist files to extension-release...");
 | 
			
		||||
    await copyDir(distDir, releaseDir);
 | 
			
		||||
 | 
			
		||||
    // 6. Copy icon directory to release (merged at root)
 | 
			
		||||
    console.log("📦 Copying icon directory to extension-release...");
 | 
			
		||||
    await copyDir(iconDir, path.join(releaseDir, "icons"));
 | 
			
		||||
 | 
			
		||||
    console.log("✅ Build complete! extension-release ready.");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
main().catch((e) => {
 | 
			
		||||
    console.error("❌ Build failed:", e);
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										67
									
								
								content.js
									
									
									
									
									
								
							
							
						
						
									
										67
									
								
								content.js
									
									
									
									
									
								
							@ -1,67 +0,0 @@
 | 
			
		||||
// Inject `inject.js` into the page context
 | 
			
		||||
(function injectScript() {
 | 
			
		||||
  const script = document.createElement('script');
 | 
			
		||||
  script.src = chrome.runtime.getURL('inject.js');
 | 
			
		||||
  script.type = 'text/javascript';
 | 
			
		||||
  script.onload = () => script.remove(); // Clean up
 | 
			
		||||
  // Inject directly into <html> or <head>
 | 
			
		||||
  (document.documentElement || document.head || document.body).appendChild(script);
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
// Listen for messages from the injected script
 | 
			
		||||
window.addEventListener("message", function(event) {
 | 
			
		||||
    if (event.source !== window) return;
 | 
			
		||||
 | 
			
		||||
    if (["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes(event.data?.type)) {
 | 
			
		||||
        chrome.runtime.sendMessage({
 | 
			
		||||
            type: event.data.type.replace("__", "").replace("__", ""),
 | 
			
		||||
            data: event.data.data
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data.type === "__GET_CDM_DEVICES__") {
 | 
			
		||||
 | 
			
		||||
      chrome.storage.local.get(["widevine_device", "playready_device"], (result) => {
 | 
			
		||||
        const widevine_device = result.widevine_device || null;
 | 
			
		||||
        const playready_device = result.playready_device || null;
 | 
			
		||||
 | 
			
		||||
        window.postMessage(
 | 
			
		||||
          {
 | 
			
		||||
            type: "__CDM_DEVICES__",
 | 
			
		||||
            widevine_device,
 | 
			
		||||
            playready_device
 | 
			
		||||
          },
 | 
			
		||||
          "*"
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data.type === "__GET_INJECTION_TYPE__") {
 | 
			
		||||
 | 
			
		||||
      chrome.storage.local.get("injection_type", (result) => {
 | 
			
		||||
        const injectionType = result.injection_type || "LICENSE";
 | 
			
		||||
 | 
			
		||||
        window.postMessage(
 | 
			
		||||
          {
 | 
			
		||||
            type: "__INJECTION_TYPE__",
 | 
			
		||||
            injectionType
 | 
			
		||||
          },
 | 
			
		||||
          "*"
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (event.data.type === "__GET_DRM_OVERRIDE__") {
 | 
			
		||||
 | 
			
		||||
      chrome.storage.local.get("drm_override", (result) => {
 | 
			
		||||
        const drmOverride = result.drm_override || "DISABLED";
 | 
			
		||||
 | 
			
		||||
        window.postMessage(
 | 
			
		||||
          {
 | 
			
		||||
            type: "__DRM_OVERRIDE__",
 | 
			
		||||
            drmOverride
 | 
			
		||||
          },
 | 
			
		||||
          "*"
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										9
									
								
								frontend/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
    "trailingComma": "es5",
 | 
			
		||||
    "tabWidth": 4,
 | 
			
		||||
    "semi": true,
 | 
			
		||||
    "singleQuote": false,
 | 
			
		||||
    "useTabs": false,
 | 
			
		||||
    "printWidth": 100,
 | 
			
		||||
    "plugins": ["prettier-plugin-tailwindcss"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/dist/assets/index-UaipKa9p.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/dist/assets/index-UaipKa9p.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										13
									
								
								frontend/dist/index.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								frontend/dist/index.html
									
									
									
									
										vendored
									
									
								
							@ -1,13 +0,0 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>CDRM Decryption Extension</title>
 | 
			
		||||
    <script type="module" crossorigin src="./assets/index-ydPQKJSy.js"></script>
 | 
			
		||||
    <link rel="stylesheet" crossorigin href="./assets/index-UaipKa9p.css">
 | 
			
		||||
  </head>
 | 
			
		||||
  <body class="min-w-full min-h-full w-full h-full">
 | 
			
		||||
    <div class="min-w-full min-h-full w-full h-full" id="root"></div>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
@ -1,33 +1,30 @@
 | 
			
		||||
import js from '@eslint/js'
 | 
			
		||||
import globals from 'globals'
 | 
			
		||||
import reactHooks from 'eslint-plugin-react-hooks'
 | 
			
		||||
import reactRefresh from 'eslint-plugin-react-refresh'
 | 
			
		||||
import js from "@eslint/js";
 | 
			
		||||
import globals from "globals";
 | 
			
		||||
import reactHooks from "eslint-plugin-react-hooks";
 | 
			
		||||
import reactRefresh from "eslint-plugin-react-refresh";
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
  { ignores: ['dist'] },
 | 
			
		||||
    { ignores: ["dist"] },
 | 
			
		||||
    {
 | 
			
		||||
    files: ['**/*.{js,jsx}'],
 | 
			
		||||
        files: ["**/*.{js,jsx}"],
 | 
			
		||||
        languageOptions: {
 | 
			
		||||
            ecmaVersion: 2020,
 | 
			
		||||
            globals: globals.browser,
 | 
			
		||||
            parserOptions: {
 | 
			
		||||
        ecmaVersion: 'latest',
 | 
			
		||||
                ecmaVersion: "latest",
 | 
			
		||||
                ecmaFeatures: { jsx: true },
 | 
			
		||||
        sourceType: 'module',
 | 
			
		||||
                sourceType: "module",
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        plugins: {
 | 
			
		||||
      'react-hooks': reactHooks,
 | 
			
		||||
      'react-refresh': reactRefresh,
 | 
			
		||||
            "react-hooks": reactHooks,
 | 
			
		||||
            "react-refresh": reactRefresh,
 | 
			
		||||
        },
 | 
			
		||||
        rules: {
 | 
			
		||||
            ...js.configs.recommended.rules,
 | 
			
		||||
            ...reactHooks.configs.recommended.rules,
 | 
			
		||||
      'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
 | 
			
		||||
      'react-refresh/only-export-components': [
 | 
			
		||||
        'warn',
 | 
			
		||||
        { allowConstantExport: true },
 | 
			
		||||
      ],
 | 
			
		||||
            "no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
 | 
			
		||||
            "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
]
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,8 @@
 | 
			
		||||
<html lang="en">
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>CDRM Decryption Extension</title>
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										869
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										869
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "frontend",
 | 
			
		||||
    "private": true,
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
    "version": "3.0",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "dev": "vite",
 | 
			
		||||
@ -10,21 +10,26 @@
 | 
			
		||||
        "preview": "vite preview"
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
    "@tailwindcss/vite": "^4.1.7",
 | 
			
		||||
        "@tailwindcss/vite": "^4.1.11",
 | 
			
		||||
        "react": "^19.1.0",
 | 
			
		||||
        "react-dom": "^19.1.0",
 | 
			
		||||
    "react-router-dom": "^7.6.1",
 | 
			
		||||
    "tailwindcss": "^4.1.7"
 | 
			
		||||
        "react-icons": "^5.5.0",
 | 
			
		||||
        "react-router-dom": "^7.7.0",
 | 
			
		||||
        "sonner": "^2.0.6",
 | 
			
		||||
        "tailwindcss": "^4.1.11"
 | 
			
		||||
    },
 | 
			
		||||
    "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/js": "^9.31.0",
 | 
			
		||||
        "@types/react": "^19.1.8",
 | 
			
		||||
        "@types/react-dom": "^19.1.6",
 | 
			
		||||
        "@vitejs/plugin-react": "^4.7.0",
 | 
			
		||||
        "@vitejs/plugin-react-swc": "^3.11.0",
 | 
			
		||||
        "daisyui": "^5.0.46",
 | 
			
		||||
        "eslint": "^9.31.0",
 | 
			
		||||
        "eslint-plugin-react-hooks": "^5.2.0",
 | 
			
		||||
    "eslint-plugin-react-refresh": "^0.4.19",
 | 
			
		||||
    "globals": "^16.0.0",
 | 
			
		||||
    "vite": "^6.3.5"
 | 
			
		||||
        "eslint-plugin-react-refresh": "^0.4.20",
 | 
			
		||||
        "globals": "^16.3.0",
 | 
			
		||||
        "prettier-plugin-tailwindcss": "^0.6.14",
 | 
			
		||||
        "vite": "^7.0.5"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,33 +1,41 @@
 | 
			
		||||
import { useState, useEffect } from "react";
 | 
			
		||||
import {
 | 
			
		||||
  HashRouter as Router,
 | 
			
		||||
  Routes,
 | 
			
		||||
  Route,
 | 
			
		||||
  Navigate,
 | 
			
		||||
} from "react-router-dom";
 | 
			
		||||
import TopNav from "./components/topnav";
 | 
			
		||||
import SideNav from "./components/sidenav";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { Navigate, Route, HashRouter as Router, Routes } from "react-router-dom";
 | 
			
		||||
import About from "./components/about";
 | 
			
		||||
import Container from "./components/container";
 | 
			
		||||
import Results from "./components/results";
 | 
			
		||||
import Settings from "./components/settings";
 | 
			
		||||
import TabNavigation from "./components/tabnavigation";
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  const [isSideNavOpen, setIsSideNavOpen] = useState(false);
 | 
			
		||||
  const [validConfig, setValidConfig] = useState(null); // null = loading
 | 
			
		||||
const App = () => {
 | 
			
		||||
    const [validConfig, setValidConfig] = useState(null);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // Fix: Access chrome API properly for browser extensions
 | 
			
		||||
        if (typeof chrome !== "undefined" && chrome.storage) {
 | 
			
		||||
            chrome.storage.local.get("valid_config", (result) => {
 | 
			
		||||
                if (chrome.runtime.lastError) {
 | 
			
		||||
                    console.error("Error reading valid_config:", chrome.runtime.lastError);
 | 
			
		||||
        setValidConfig(false); // fallback
 | 
			
		||||
                    setValidConfig(false);
 | 
			
		||||
                } else {
 | 
			
		||||
                    setValidConfig(result.valid_config === true);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            // Fallback for development/testing
 | 
			
		||||
            setValidConfig(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const handleConfigSaved = () => {
 | 
			
		||||
        setValidConfig(true);
 | 
			
		||||
        // Navigate to main tab after config is saved
 | 
			
		||||
        window.location.hash = "#/results";
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (validConfig === null) {
 | 
			
		||||
        return (
 | 
			
		||||
      <div className="flex items-center justify-center h-screen bg-black text-white">
 | 
			
		||||
            <div className="flex h-screen items-center justify-center">
 | 
			
		||||
                <span className="loading loading-spinner loading-md ms-2"></span>
 | 
			
		||||
                Loading...
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
@ -35,20 +43,16 @@ function App() {
 | 
			
		||||
 | 
			
		||||
    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">
 | 
			
		||||
            <div className="flex h-screen flex-col py-4">
 | 
			
		||||
                <Container>
 | 
			
		||||
                    <TabNavigation validConfig={validConfig} />
 | 
			
		||||
                    <div className="divider"></div>
 | 
			
		||||
                    <Routes>
 | 
			
		||||
                        {!validConfig ? (
 | 
			
		||||
                            <>
 | 
			
		||||
                                <Route
 | 
			
		||||
                                    path="/settings"
 | 
			
		||||
                  element={
 | 
			
		||||
                    <Settings onConfigSaved={() => setValidConfig(true)} />
 | 
			
		||||
                  }
 | 
			
		||||
                                    element={<Settings onConfigSaved={handleConfigSaved} />}
 | 
			
		||||
                                />
 | 
			
		||||
                                <Route path="*" element={<Navigate to="/settings" replace />} />
 | 
			
		||||
                            </>
 | 
			
		||||
@ -57,21 +61,14 @@ function App() {
 | 
			
		||||
                                <Route path="/" element={<Navigate to="/results" replace />} />
 | 
			
		||||
                                <Route path="/results" element={<Results />} />
 | 
			
		||||
                                <Route path="/settings" element={<Settings />} />
 | 
			
		||||
                                <Route path="/about" element={<About />} />
 | 
			
		||||
                            </>
 | 
			
		||||
                        )}
 | 
			
		||||
                    </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>
 | 
			
		||||
                </Container>
 | 
			
		||||
            </div>
 | 
			
		||||
        </Router>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default App;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								frontend/src/assets/fonts/InterVariable-Italic.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/assets/fonts/InterVariable-Italic.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								frontend/src/assets/fonts/InterVariable.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/assets/fonts/InterVariable.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										15
									
								
								frontend/src/assets/fonts/font-face.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/assets/fonts/font-face.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: Inter;
 | 
			
		||||
    src: url("./InterVariable.woff2");
 | 
			
		||||
    font-style: normal;
 | 
			
		||||
    font-weight: 300 900;
 | 
			
		||||
    font-display: swap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
    font-family: Inter;
 | 
			
		||||
    src: url("./InterVariable-Italic.woff2");
 | 
			
		||||
    font-style: italic;
 | 
			
		||||
    font-weight: 300 900;
 | 
			
		||||
    font-display: swap;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								frontend/src/components/about.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								frontend/src/components/about.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
			
		||||
import { FaDiscord, FaTelegram } from "react-icons/fa";
 | 
			
		||||
import { SiGitea } from "react-icons/si";
 | 
			
		||||
 | 
			
		||||
const AboutPage = () => {
 | 
			
		||||
    const socialLinks = [
 | 
			
		||||
        {
 | 
			
		||||
            name: "Discord",
 | 
			
		||||
            icon: <FaDiscord className="text-4xl" />,
 | 
			
		||||
            url: "https://discord.cdrm-project.com/",
 | 
			
		||||
            description: "Join our Discord community",
 | 
			
		||||
            color: "hover:text-indigo-400",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: "Telegram",
 | 
			
		||||
            icon: <FaTelegram className="text-4xl" />,
 | 
			
		||||
            url: "https://telegram.cdrm-project.com/",
 | 
			
		||||
            description: "Follow us on Telegram",
 | 
			
		||||
            color: "hover:text-sky-400",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: "Gitea",
 | 
			
		||||
            icon: <SiGitea className="text-4xl" />,
 | 
			
		||||
            url: "https://cdm-project.com/tpd94/cdrm-project",
 | 
			
		||||
            description: "Check out our code",
 | 
			
		||||
            color: "hover:text-lime-400",
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="flex min-h-full flex-col items-center justify-center p-6">
 | 
			
		||||
            <div className="mb-8 text-center">
 | 
			
		||||
                <h2 className="mb-2 text-3xl font-bold">Connect with us</h2>
 | 
			
		||||
                <p className="text-base-content/70 text-lg">Join our community and stay updated</p>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className="grid w-full max-w-4xl grid-cols-1 gap-6 md:grid-cols-3">
 | 
			
		||||
                {socialLinks.map((link) => (
 | 
			
		||||
                    <a
 | 
			
		||||
                        key={link.name}
 | 
			
		||||
                        href={link.url}
 | 
			
		||||
                        target="_blank"
 | 
			
		||||
                        rel="noopener noreferrer"
 | 
			
		||||
                        className={`card bg-base-200 shadow-xl transition-all duration-300 hover:scale-105 hover:shadow-2xl ${link.color}`}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div className="card-body items-center text-center">
 | 
			
		||||
                            <div className="mb-2">{link.icon}</div>
 | 
			
		||||
                            <h3 className="card-title text-xl font-semibold">{link.name}</h3>
 | 
			
		||||
                            <p className="text-base-content/70">{link.description}</p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </a>
 | 
			
		||||
                ))}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AboutPage;
 | 
			
		||||
							
								
								
									
										9
									
								
								frontend/src/components/container.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/components/container.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
const Container = ({ children, className = "", ...props }) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <main className={`container mx-auto p-4 mb-5 ${className}`} {...props}>
 | 
			
		||||
            {children}
 | 
			
		||||
        </main>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Container;
 | 
			
		||||
							
								
								
									
										72
									
								
								frontend/src/components/injectionmenu.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								frontend/src/components/injectionmenu.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
const InjectionMenu = () => {
 | 
			
		||||
    const [injectionType, setInjectionType] = useState("LICENSE");
 | 
			
		||||
    const [drmOverride, setDrmOverride] = useState("DISABLED");
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        chrome.storage.local.get(["injection_type", "drm_override"], (result) => {
 | 
			
		||||
            if (result.injection_type !== undefined) {
 | 
			
		||||
                setInjectionType(result.injection_type);
 | 
			
		||||
            }
 | 
			
		||||
            if (result.drm_override !== undefined) {
 | 
			
		||||
                setDrmOverride(result.drm_override);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const handleInjectionTypeChange = (type) => {
 | 
			
		||||
        chrome.storage.local.set({ injection_type: type }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("Error updating injection_type:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                setInjectionType(type);
 | 
			
		||||
                console.log(`Injection type updated to ${type}`);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleDrmOverrideChange = (type) => {
 | 
			
		||||
        chrome.storage.local.set({ drm_override: type }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("Error updating drm_override:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                setDrmOverride(type);
 | 
			
		||||
                console.log(`DRM Override updated to ${type}`);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="flex flex-row">
 | 
			
		||||
            <div className="mr-2 ml-auto flex h-full flex-row items-center justify-center">
 | 
			
		||||
                <p className="mr-2 p-2 text-lg text-nowrap">Injection type:</p>
 | 
			
		||||
                <div role="tablist" className="tabs tabs-border">
 | 
			
		||||
                    <a
 | 
			
		||||
                        role="tab"
 | 
			
		||||
                        className={`tab ${injectionType === "LICENSE" ? "tab-active font-semibold" : ""}`}
 | 
			
		||||
                        onClick={() => handleInjectionTypeChange("LICENSE")}
 | 
			
		||||
                    >
 | 
			
		||||
                        License
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a
 | 
			
		||||
                        role="tab"
 | 
			
		||||
                        className={`tab ${injectionType === "EME" ? "tab-active font-semibold" : ""}`}
 | 
			
		||||
                        onClick={() => handleInjectionTypeChange("EME")}
 | 
			
		||||
                    >
 | 
			
		||||
                        EME
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <a
 | 
			
		||||
                        role="tab"
 | 
			
		||||
                        className={`tab ${injectionType === "DISABLED" ? "tab-active font-semibold" : ""}`}
 | 
			
		||||
                        onClick={() => handleInjectionTypeChange("DISABLED")}
 | 
			
		||||
                    >
 | 
			
		||||
                        Disabled
 | 
			
		||||
                    </a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default InjectionMenu;
 | 
			
		||||
@ -1,10 +1,15 @@
 | 
			
		||||
import React, { useEffect, useState } from "react";
 | 
			
		||||
import { IoCameraOutline, IoCopyOutline, IoSaveOutline } from "react-icons/io5";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
import InjectionMenu from "./injectionmenu";
 | 
			
		||||
 | 
			
		||||
function Results() {
 | 
			
		||||
const Results = () => {
 | 
			
		||||
    const [drmType, setDrmType] = useState("");
 | 
			
		||||
    const [pssh, setPssh] = useState("");
 | 
			
		||||
    const [licenseUrl, setLicenseUrl] = useState("");
 | 
			
		||||
    const [keys, setKeys] = useState([]);
 | 
			
		||||
    const [manifestUrl, setManifestUrl] = useState("");
 | 
			
		||||
    const [currentTabUrl, setCurrentTabUrl] = useState("");
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        chrome.storage.local.get(
 | 
			
		||||
@ -14,11 +19,13 @@ function Results() {
 | 
			
		||||
                "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.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)
 | 
			
		||||
@ -33,19 +40,37 @@ function Results() {
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Get current tab URL when component mounts
 | 
			
		||||
        chrome.windows.getAll({ populate: true, windowTypes: ["normal"] }, (windows) => {
 | 
			
		||||
            if (windows && windows.length > 0) {
 | 
			
		||||
                const lastFocusedWindow = windows.find((w) => w.focused) || windows[0];
 | 
			
		||||
                if (lastFocusedWindow) {
 | 
			
		||||
                    const activeTab = lastFocusedWindow.tabs.find(
 | 
			
		||||
                        (tab) => tab.active && tab.url && /^https?:\/\//.test(tab.url)
 | 
			
		||||
                    );
 | 
			
		||||
                    if (activeTab?.url) {
 | 
			
		||||
                        setCurrentTabUrl(activeTab.url);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const handleChange = (changes, area) => {
 | 
			
		||||
            if (area === "local") {
 | 
			
		||||
                if (changes.drmType) {
 | 
			
		||||
          setDrmType(changes.drmType.newValue);
 | 
			
		||||
                    setDrmType(changes.drmType.newValue || "");
 | 
			
		||||
                }
 | 
			
		||||
                if (changes.latestPSSH) {
 | 
			
		||||
          setPssh(changes.latestPSSH.newValue);
 | 
			
		||||
                    setPssh(changes.latestPSSH.newValue || "");
 | 
			
		||||
                }
 | 
			
		||||
                if (changes.licenseURL) {
 | 
			
		||||
          setLicenseUrl(changes.licenseURL.newValue);
 | 
			
		||||
                    setLicenseUrl(changes.licenseURL.newValue || "");
 | 
			
		||||
                }
 | 
			
		||||
                if (changes.manifestURL) {
 | 
			
		||||
                    setManifestUrl(changes.manifestURL.newValue || "");
 | 
			
		||||
                }
 | 
			
		||||
                if (changes.latestKeys) {
 | 
			
		||||
          setKeys(changes.latestKeys.newValue);
 | 
			
		||||
                    setKeys(changes.latestKeys.newValue || []);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
@ -57,16 +82,15 @@ function Results() {
 | 
			
		||||
    const handleCapture = () => {
 | 
			
		||||
        // Reset stored values
 | 
			
		||||
        chrome.storage.local.set({
 | 
			
		||||
      drmType: "None",
 | 
			
		||||
      latestPSSH: "None",
 | 
			
		||||
      licenseURL: "None",
 | 
			
		||||
            drmType: "",
 | 
			
		||||
            latestPSSH: "",
 | 
			
		||||
            licenseURL: "",
 | 
			
		||||
            manifestURL: "",
 | 
			
		||||
            latestKeys: [],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Get all normal windows to exclude your popup
 | 
			
		||||
    chrome.windows.getAll(
 | 
			
		||||
      { populate: true, windowTypes: ["normal"] },
 | 
			
		||||
      (windows) => {
 | 
			
		||||
        chrome.windows.getAll({ populate: true, windowTypes: ["normal"] }, (windows) => {
 | 
			
		||||
            if (!windows || windows.length === 0) {
 | 
			
		||||
                console.warn("No normal Chrome windows found");
 | 
			
		||||
                return;
 | 
			
		||||
@ -94,56 +118,204 @@ function Results() {
 | 
			
		||||
            } else {
 | 
			
		||||
                console.warn("No active tab found in the last focused normal window");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleCopyToClipboard = (text, label) => {
 | 
			
		||||
        navigator.clipboard.writeText(text);
 | 
			
		||||
        toast.success(`Copied ${label} to clipboard`);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Check if current tab is YouTube
 | 
			
		||||
    const isYouTube = () => {
 | 
			
		||||
        return currentTabUrl.includes("youtube.com") || currentTabUrl.includes("youtu.be");
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Get manifest URL display value
 | 
			
		||||
    const getManifestDisplayValue = () => {
 | 
			
		||||
        if (manifestUrl) {
 | 
			
		||||
            return manifestUrl;
 | 
			
		||||
        }
 | 
			
		||||
    );
 | 
			
		||||
        if (isYouTube()) {
 | 
			
		||||
            return "[Use yt-dlp to download video]";
 | 
			
		||||
        }
 | 
			
		||||
        return "";
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Get manifest URL placeholder
 | 
			
		||||
    const getManifestPlaceholder = () => {
 | 
			
		||||
        if (isYouTube() && !manifestUrl) {
 | 
			
		||||
            return "[Use yt-dlp to download video]";
 | 
			
		||||
        }
 | 
			
		||||
        return "[Not available]";
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Export to JSON file
 | 
			
		||||
    const hasData = () => {
 | 
			
		||||
        return Array.isArray(keys) && keys.filter((k) => k.type !== "SIGNING").length > 0;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleExportJSON = () => {
 | 
			
		||||
        const exportData = {
 | 
			
		||||
            drmType: drmType || null,
 | 
			
		||||
            manifestUrl: manifestUrl || null,
 | 
			
		||||
            pssh: pssh || null,
 | 
			
		||||
            licenseUrl: licenseUrl || null,
 | 
			
		||||
            keys:
 | 
			
		||||
                Array.isArray(keys) && keys.length > 0
 | 
			
		||||
                    ? keys
 | 
			
		||||
                          .filter((k) => k.type !== "SIGNING")
 | 
			
		||||
                          .map((k) => `${k.key_id || k.keyId}:${k.key}`)
 | 
			
		||||
                    : null,
 | 
			
		||||
            exportedAt: new Date().toISOString(),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const blob = new Blob([JSON.stringify(exportData, null, 2)], {
 | 
			
		||||
            type: "application/json",
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const url = URL.createObjectURL(blob);
 | 
			
		||||
        const a = document.createElement("a");
 | 
			
		||||
        a.href = url;
 | 
			
		||||
        a.download = `drm-data-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json`;
 | 
			
		||||
        document.body.appendChild(a);
 | 
			
		||||
        a.click();
 | 
			
		||||
        document.body.removeChild(a);
 | 
			
		||||
        URL.revokeObjectURL(url);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
    <div className="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"
 | 
			
		||||
      >
 | 
			
		||||
        <div className="flex h-full w-full flex-col gap-4">
 | 
			
		||||
            <InjectionMenu />
 | 
			
		||||
            <button onClick={handleCapture} className="btn btn-primary">
 | 
			
		||||
                <IoCameraOutline className="h-5 w-5" />
 | 
			
		||||
                Capture current tab
 | 
			
		||||
            </button>
 | 
			
		||||
      <p className="text-2xl mt-5">DRM Type</p>
 | 
			
		||||
 | 
			
		||||
            <fieldset className="fieldset">
 | 
			
		||||
                <legend className="fieldset-legend text-base">DRM Type</legend>
 | 
			
		||||
                <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value={drmType}
 | 
			
		||||
        className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
 | 
			
		||||
        placeholder="None"
 | 
			
		||||
                    className="input w-full font-mono"
 | 
			
		||||
                    placeholder="[Not available]"
 | 
			
		||||
                    disabled
 | 
			
		||||
                />
 | 
			
		||||
      <p className="text-2xl mt-5">PSSH</p>
 | 
			
		||||
            </fieldset>
 | 
			
		||||
 | 
			
		||||
            <fieldset className="fieldset">
 | 
			
		||||
                <legend className="fieldset-legend text-base">Manifest URL</legend>
 | 
			
		||||
                <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value={getManifestDisplayValue()}
 | 
			
		||||
                    className={`input w-full font-mono ${
 | 
			
		||||
                        isYouTube() && !manifestUrl ? "text-yellow-400" : ""
 | 
			
		||||
                    }`}
 | 
			
		||||
                    placeholder={getManifestPlaceholder()}
 | 
			
		||||
                    disabled
 | 
			
		||||
                />
 | 
			
		||||
                {!isYouTube() && manifestUrl && (
 | 
			
		||||
                    <p className="label flex justify-end">
 | 
			
		||||
                        <button
 | 
			
		||||
                            className="btn btn-link btn-sm text-info"
 | 
			
		||||
                            onClick={() => handleCopyToClipboard(manifestUrl, "manifest URL")}
 | 
			
		||||
                        >
 | 
			
		||||
                            <IoCopyOutline className="h-5 w-5" />
 | 
			
		||||
                            Copy manifest URL
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </p>
 | 
			
		||||
                )}
 | 
			
		||||
            </fieldset>
 | 
			
		||||
 | 
			
		||||
            <fieldset className="fieldset">
 | 
			
		||||
                <legend className="fieldset-legend text-base">PSSH</legend>
 | 
			
		||||
                <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value={pssh}
 | 
			
		||||
        className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
 | 
			
		||||
        placeholder="None"
 | 
			
		||||
                    className="input w-full font-mono"
 | 
			
		||||
                    placeholder="[Not available]"
 | 
			
		||||
                    disabled
 | 
			
		||||
                />
 | 
			
		||||
      <p className="text-2xl mt-5">License URL</p>
 | 
			
		||||
                {pssh && (
 | 
			
		||||
                    <p className="label flex justify-end">
 | 
			
		||||
                        <button
 | 
			
		||||
                            className="btn btn-link btn-sm text-info"
 | 
			
		||||
                            onClick={() => handleCopyToClipboard(pssh, "PSSH")}
 | 
			
		||||
                        >
 | 
			
		||||
                            <IoCopyOutline className="h-5 w-5" />
 | 
			
		||||
                            Copy PSSH
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </p>
 | 
			
		||||
                )}
 | 
			
		||||
            </fieldset>
 | 
			
		||||
 | 
			
		||||
            <fieldset className="fieldset">
 | 
			
		||||
                <legend className="fieldset-legend text-base">License URL</legend>
 | 
			
		||||
                <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value={licenseUrl}
 | 
			
		||||
        className="w-full h-10 bg-slate-800/50 rounded-md p-2 mt-2 text-white"
 | 
			
		||||
        placeholder="None"
 | 
			
		||||
                    className="input w-full 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">
 | 
			
		||||
        {Array.isArray(keys) &&
 | 
			
		||||
        keys.filter((k) => k.type !== "SIGNING").length > 0 ? (
 | 
			
		||||
          keys
 | 
			
		||||
                {licenseUrl && (
 | 
			
		||||
                    <p className="label flex justify-end">
 | 
			
		||||
                        <button
 | 
			
		||||
                            className="btn btn-link btn-sm text-info"
 | 
			
		||||
                            onClick={() => handleCopyToClipboard(licenseUrl, "license URL")}
 | 
			
		||||
                        >
 | 
			
		||||
                            <IoCopyOutline className="h-5 w-5" />
 | 
			
		||||
                            Copy license URL
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </p>
 | 
			
		||||
                )}
 | 
			
		||||
            </fieldset>
 | 
			
		||||
 | 
			
		||||
            <fieldset className="fieldset">
 | 
			
		||||
                <legend className="fieldset-legend text-base">Keys</legend>
 | 
			
		||||
                <textarea
 | 
			
		||||
                    value={
 | 
			
		||||
                        Array.isArray(keys) && keys.filter((k) => k.type !== "SIGNING").length > 0
 | 
			
		||||
                            ? keys
 | 
			
		||||
                                  .filter((k) => k.type !== "SIGNING")
 | 
			
		||||
                                  .map((k) => `${k.key_id || k.keyId}:${k.key}`)
 | 
			
		||||
                                  .join("\n")
 | 
			
		||||
        ) : (
 | 
			
		||||
          <span className="text-gray-400">None</span>
 | 
			
		||||
                            : "[Not available]"
 | 
			
		||||
                    }
 | 
			
		||||
                    className="textarea w-full font-mono"
 | 
			
		||||
                    disabled
 | 
			
		||||
                />
 | 
			
		||||
                {hasData() &&
 | 
			
		||||
                    Array.isArray(keys) &&
 | 
			
		||||
                    keys.filter((k) => k.type !== "SIGNING").length > 0 && (
 | 
			
		||||
                        <p className="label flex justify-end">
 | 
			
		||||
                            <button
 | 
			
		||||
                                className="btn btn-link btn-sm text-info"
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    handleCopyToClipboard(
 | 
			
		||||
                                        keys
 | 
			
		||||
                                            .filter((k) => k.type !== "SIGNING")
 | 
			
		||||
                                            .map((k) => `${k.key_id || k.keyId}:${k.key}`)
 | 
			
		||||
                                            .join("\n"),
 | 
			
		||||
                                        "keys"
 | 
			
		||||
                                    )
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                <IoCopyOutline className="h-5 w-5" />
 | 
			
		||||
                                Copy keys
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </p>
 | 
			
		||||
                    )}
 | 
			
		||||
            </fieldset>
 | 
			
		||||
 | 
			
		||||
            {hasData() && (
 | 
			
		||||
                <button onClick={handleExportJSON} className="btn btn-success">
 | 
			
		||||
                    <IoSaveOutline className="h-5 w-5" />
 | 
			
		||||
                    Export as JSON
 | 
			
		||||
                </button>
 | 
			
		||||
            )}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Results;
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
import { useState, useEffect } from "react";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { IoSaveOutline } from "react-icons/io5";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { toast } from "sonner";
 | 
			
		||||
 | 
			
		||||
function Settings({ onConfigSaved }) {
 | 
			
		||||
const Settings = ({ onConfigSaved }) => {
 | 
			
		||||
    const [instanceUrl, setInstanceUrl] = useState("");
 | 
			
		||||
    const [storedUrl, setStoredUrl] = useState(null);
 | 
			
		||||
  const [message, setMessage] = useState(null);
 | 
			
		||||
  const [messageType, setMessageType] = useState(null);
 | 
			
		||||
    const [loading, setLoading] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
@ -13,10 +13,8 @@ function Settings({ onConfigSaved }) {
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        chrome.storage.local.get("cdrm_instance", (result) => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
        console.error(
 | 
			
		||||
          "Error fetching CDRM instance:",
 | 
			
		||||
          chrome.runtime.lastError
 | 
			
		||||
        );
 | 
			
		||||
                toast.error("Error fetching CDRM instance:", chrome.runtime.lastError);
 | 
			
		||||
                console.error("Error fetching CDRM instance:", chrome.runtime.lastError);
 | 
			
		||||
            } else if (result.cdrm_instance) {
 | 
			
		||||
                setStoredUrl(result.cdrm_instance);
 | 
			
		||||
            }
 | 
			
		||||
@ -26,14 +24,12 @@ function Settings({ onConfigSaved }) {
 | 
			
		||||
    const handleSave = async () => {
 | 
			
		||||
        const trimmedUrl = instanceUrl.trim().replace(/\/+$/, "");
 | 
			
		||||
        if (!trimmedUrl) {
 | 
			
		||||
      setMessage("Please enter a valid URL.");
 | 
			
		||||
      setMessageType("error");
 | 
			
		||||
            toast.error("Please enter a valid URL.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const endpoint = trimmedUrl + "/api/extension";
 | 
			
		||||
        setLoading(true);
 | 
			
		||||
    setMessage(null);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const response = await fetch(endpoint, {
 | 
			
		||||
@ -46,21 +42,24 @@ function Settings({ onConfigSaved }) {
 | 
			
		||||
            const data = await response.json();
 | 
			
		||||
 | 
			
		||||
            if (data.status === true) {
 | 
			
		||||
        setMessage("Successfully connected to CDRM Instance.");
 | 
			
		||||
        setMessageType("success");
 | 
			
		||||
                toast.success("Successfully connected to a CDRM instance");
 | 
			
		||||
 | 
			
		||||
        const widevineRes = await fetch(
 | 
			
		||||
          `${trimmedUrl}/remotecdm/widevine/deviceinfo`
 | 
			
		||||
                const widevineRes = await fetch(`${trimmedUrl}/remotecdm/widevine/deviceinfo`);
 | 
			
		||||
                if (!widevineRes.ok) {
 | 
			
		||||
                    toast.error(
 | 
			
		||||
                        `Failed to fetch Widevine device info. Reason: ${widevineRes.statusText}`
 | 
			
		||||
                    );
 | 
			
		||||
        if (!widevineRes.ok)
 | 
			
		||||
          throw new Error("Failed to fetch Widevine device info");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                const widevineData = await widevineRes.json();
 | 
			
		||||
 | 
			
		||||
        const playreadyRes = await fetch(
 | 
			
		||||
          `${trimmedUrl}/remotecdm/playready/deviceinfo`
 | 
			
		||||
                const playreadyRes = await fetch(`${trimmedUrl}/remotecdm/playready/deviceinfo`);
 | 
			
		||||
                if (!playreadyRes.ok) {
 | 
			
		||||
                    toast.error(
 | 
			
		||||
                        `Failed to fetch PlayReady device info. Reason: ${playreadyRes.statusText}`
 | 
			
		||||
                    );
 | 
			
		||||
        if (!playreadyRes.ok)
 | 
			
		||||
          throw new Error("Failed to fetch PlayReady device info");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                const playreadyData = await playreadyRes.json();
 | 
			
		||||
 | 
			
		||||
                chrome.storage.local.set(
 | 
			
		||||
@ -88,10 +87,11 @@ function Settings({ onConfigSaved }) {
 | 
			
		||||
                                "Error saving to chrome.storage:",
 | 
			
		||||
                                chrome.runtime.lastError
 | 
			
		||||
                            );
 | 
			
		||||
              setMessage("Error saving configuration.");
 | 
			
		||||
              setMessageType("error");
 | 
			
		||||
                            toast.error(
 | 
			
		||||
                                `Error saving configuration. Reason: ${chrome.runtime.lastError}`
 | 
			
		||||
                            );
 | 
			
		||||
                        } else {
 | 
			
		||||
              console.log("Configuration saved.");
 | 
			
		||||
                            console.log("Configuration saved");
 | 
			
		||||
                            setStoredUrl(trimmedUrl);
 | 
			
		||||
                            setInstanceUrl("");
 | 
			
		||||
                            if (onConfigSaved) onConfigSaved();
 | 
			
		||||
@ -100,51 +100,56 @@ function Settings({ onConfigSaved }) {
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
            } else {
 | 
			
		||||
        throw new Error("Invalid response from endpoint.");
 | 
			
		||||
                toast.error("Invalid response from endpoint.");
 | 
			
		||||
            }
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            console.error("Connection error:", err);
 | 
			
		||||
      setMessage("Invalid endpoint or device info could not be retrieved.");
 | 
			
		||||
      setMessageType("error");
 | 
			
		||||
            toast.error(
 | 
			
		||||
                `Invalid endpoint or device info could not be retrieved. Reason: ${err.message}`
 | 
			
		||||
            );
 | 
			
		||||
        } finally {
 | 
			
		||||
            setLoading(false);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
    <div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col p-4">
 | 
			
		||||
        <div className="flex h-full w-full flex-col gap-4">
 | 
			
		||||
            {storedUrl && (
 | 
			
		||||
                <p className="mb-2 text-base">
 | 
			
		||||
                    Current instance: <span className="font-mono font-semibold">{storedUrl}</span>
 | 
			
		||||
                </p>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
            <fieldset className="fieldset">
 | 
			
		||||
                <legend className="fieldset-legend text-base">New instance URL</legend>
 | 
			
		||||
                <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value={instanceUrl}
 | 
			
		||||
                    onChange={(e) => setInstanceUrl(e.target.value)}
 | 
			
		||||
        placeholder={
 | 
			
		||||
          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"
 | 
			
		||||
                    placeholder="https://cdrm-project.com/, http://127.0.0.1:5000/"
 | 
			
		||||
                    className="input w-full font-mono"
 | 
			
		||||
                />
 | 
			
		||||
            </fieldset>
 | 
			
		||||
 | 
			
		||||
            <button
 | 
			
		||||
                type="button"
 | 
			
		||||
                onClick={handleSave}
 | 
			
		||||
                disabled={loading}
 | 
			
		||||
        className={`mt-4 p-2 ${
 | 
			
		||||
          loading ? "bg-blue-400" : "bg-blue-600 hover:bg-blue-700"
 | 
			
		||||
        } text-white rounded-md transition duration-300`}
 | 
			
		||||
                className="btn btn-primary btn-block"
 | 
			
		||||
            >
 | 
			
		||||
        {loading ? "Connecting..." : "Save Settings"}
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      {message && (
 | 
			
		||||
        <p
 | 
			
		||||
          className={`mt-2 text-sm text-center ${
 | 
			
		||||
            messageType === "success" ? "text-green-400" : "text-red-400"
 | 
			
		||||
          }`}
 | 
			
		||||
        >
 | 
			
		||||
          {message}
 | 
			
		||||
        </p>
 | 
			
		||||
                {loading ? (
 | 
			
		||||
                    <>
 | 
			
		||||
                        <span className="loading loading-spinner loading-sm"></span> Connecting...
 | 
			
		||||
                    </>
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <>
 | 
			
		||||
                        <IoSaveOutline className="h-5 w-5" />
 | 
			
		||||
                        Save settings
 | 
			
		||||
                    </>
 | 
			
		||||
                )}
 | 
			
		||||
            </button>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Settings;
 | 
			
		||||
 | 
			
		||||
@ -1,51 +0,0 @@
 | 
			
		||||
import { NavLink } from "react-router-dom";
 | 
			
		||||
import homeIcon from "../assets/home.svg";
 | 
			
		||||
import settingsIcon from "../assets/settings.svg";
 | 
			
		||||
import closeIcon from "../assets/close.svg";
 | 
			
		||||
 | 
			
		||||
function SideNav({ onClose }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="w-full h-full overflow-y-auto overflow-x-auto flex flex-col bg-black">
 | 
			
		||||
      <div className="w-full min-h-16 max-h-16 h-16 shrink-0 flex sticky top-0 z-20 border-b border-b-white bg-black">
 | 
			
		||||
        <button
 | 
			
		||||
          onClick={onClose}
 | 
			
		||||
          className="h-full ml-auto p-3 hover:cursor-pointer"
 | 
			
		||||
        >
 | 
			
		||||
          <img src={closeIcon} alt="Close" className="h-full" />
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="w-full h-16 flex items-center justify-center mt-2">
 | 
			
		||||
        <NavLink
 | 
			
		||||
          to="/results"
 | 
			
		||||
          onClick={onClose}
 | 
			
		||||
          className="text-white text-2xl font-bold flex flex-row items-center border-l-white hover:border-l-1 w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
 | 
			
		||||
        >
 | 
			
		||||
          <img
 | 
			
		||||
            src={homeIcon}
 | 
			
		||||
            alt="Home"
 | 
			
		||||
            className="h-full w-16 p-2 flex items-center cursor-pointer"
 | 
			
		||||
          />
 | 
			
		||||
          Home
 | 
			
		||||
        </NavLink>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="w-full h-16 flex items-center justify-center mt-2">
 | 
			
		||||
        <NavLink
 | 
			
		||||
          to="/settings"
 | 
			
		||||
          onClick={onClose}
 | 
			
		||||
          className="text-white text-2xl font-bold flex flex-row items-center hover:border-l-1 border-l-white w-full hover:bg-black/50 transition duration-300 ease-in-out p-2"
 | 
			
		||||
        >
 | 
			
		||||
          <img
 | 
			
		||||
            src={settingsIcon}
 | 
			
		||||
            alt="Settings"
 | 
			
		||||
            className="h-full w-16 p-2 flex items-center cursor-pointer"
 | 
			
		||||
          />
 | 
			
		||||
          Settings
 | 
			
		||||
        </NavLink>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SideNav;
 | 
			
		||||
							
								
								
									
										51
									
								
								frontend/src/components/tabnavigation.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/src/components/tabnavigation.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
import { IoIosInformationCircleOutline } from "react-icons/io";
 | 
			
		||||
import { IoHomeOutline, IoSettingsOutline } from "react-icons/io5";
 | 
			
		||||
import { NavLink, useLocation } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
const TabNavigation = ({ validConfig }) => {
 | 
			
		||||
    const location = useLocation();
 | 
			
		||||
    const activeTab =
 | 
			
		||||
        location.pathname === "/settings"
 | 
			
		||||
            ? "settings"
 | 
			
		||||
            : location.pathname === "/about"
 | 
			
		||||
              ? "about"
 | 
			
		||||
              : "main";
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="mb-4 flex items-center justify-center">
 | 
			
		||||
            <div role="tablist" className="tabs tabs-box">
 | 
			
		||||
                <NavLink
 | 
			
		||||
                    role="tab"
 | 
			
		||||
                    to="/results"
 | 
			
		||||
                    className={`tab ${!validConfig ? "cursor-not-allowed" : activeTab === "main" ? "tab-active font-semibold" : ""}`}
 | 
			
		||||
                    onClick={(e) => {
 | 
			
		||||
                        if (!validConfig) {
 | 
			
		||||
                            e.preventDefault();
 | 
			
		||||
                        }
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <IoHomeOutline className="mr-1 h-5 w-5" />
 | 
			
		||||
                    Main
 | 
			
		||||
                </NavLink>
 | 
			
		||||
                <NavLink
 | 
			
		||||
                    role="tab"
 | 
			
		||||
                    to="/settings"
 | 
			
		||||
                    className={`tab ${activeTab === "settings" ? "tab-active font-semibold" : ""}`}
 | 
			
		||||
                >
 | 
			
		||||
                    <IoSettingsOutline className="mr-1 h-5 w-5" />
 | 
			
		||||
                    Settings
 | 
			
		||||
                </NavLink>
 | 
			
		||||
                <NavLink
 | 
			
		||||
                    role="tab"
 | 
			
		||||
                    to="/about"
 | 
			
		||||
                    className={`tab ${activeTab === "about" ? "tab-active font-semibold" : ""}`}
 | 
			
		||||
                >
 | 
			
		||||
                    <IoIosInformationCircleOutline className="mr-1 h-5 w-5" />
 | 
			
		||||
                    About
 | 
			
		||||
                </NavLink>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default TabNavigation;
 | 
			
		||||
@ -1,85 +0,0 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import hamburgerIcon from "../assets/hamburger.svg";
 | 
			
		||||
 | 
			
		||||
function TopNav({ onMenuClick }) {
 | 
			
		||||
  const [injectionType, setInjectionType] = useState("LICENSE");
 | 
			
		||||
  const [drmOverride, setDrmOverride] = useState("DISABLED");
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    chrome.storage.local.get(["injection_type", "drm_override"], (result) => {
 | 
			
		||||
      if (result.injection_type !== undefined) {
 | 
			
		||||
        setInjectionType(result.injection_type);
 | 
			
		||||
      }
 | 
			
		||||
      if (result.drm_override !== undefined) {
 | 
			
		||||
        setDrmOverride(result.drm_override);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleInjectionTypeChange = (type) => {
 | 
			
		||||
    chrome.storage.local.set({ injection_type: type }, () => {
 | 
			
		||||
      if (chrome.runtime.lastError) {
 | 
			
		||||
        console.error(
 | 
			
		||||
          "Error updating injection_type:",
 | 
			
		||||
          chrome.runtime.lastError
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        setInjectionType(type);
 | 
			
		||||
        console.log(`Injection type updated to ${type}`);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleDrmOverrideChange = (type) => {
 | 
			
		||||
    chrome.storage.local.set({ drm_override: type }, () => {
 | 
			
		||||
      if (chrome.runtime.lastError) {
 | 
			
		||||
        console.error("Error updating drm_override:", chrome.runtime.lastError);
 | 
			
		||||
      } else {
 | 
			
		||||
        setDrmOverride(type);
 | 
			
		||||
        console.log(`DRM Override updated to ${type}`);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="w-full h-full flex flex-row overflow-x-hidden">
 | 
			
		||||
      <img
 | 
			
		||||
        src={hamburgerIcon}
 | 
			
		||||
        alt="Menu"
 | 
			
		||||
        className="h-full w-16 p-2 flex items-center cursor-pointer"
 | 
			
		||||
        onClick={onMenuClick}
 | 
			
		||||
      />
 | 
			
		||||
      <div className="flex flex-row h-full justify-center items-center ml-auto mr-2">
 | 
			
		||||
        <p className="text-white text-lg p-2 mr-2 border-r-2 border-r-white text-nowrap">
 | 
			
		||||
          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;
 | 
			
		||||
@ -1,8 +1,35 @@
 | 
			
		||||
@import "tailwindcss";
 | 
			
		||||
@plugin "daisyui";
 | 
			
		||||
 | 
			
		||||
html, body, #root {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
@plugin "daisyui/theme" {
 | 
			
		||||
    name: "dim";
 | 
			
		||||
    default: true;
 | 
			
		||||
    prefersdark: true;
 | 
			
		||||
    color-scheme: "dark";
 | 
			
		||||
    --radius-selector: 0.5rem;
 | 
			
		||||
    --radius-field: 0.5rem;
 | 
			
		||||
    --radius-box: 0.5rem;
 | 
			
		||||
    --size-selector: 0.25rem;
 | 
			
		||||
    --size-field: 0.25rem;
 | 
			
		||||
    --depth: 0;
 | 
			
		||||
    --noise: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
    --font-default-sans:
 | 
			
		||||
        "Inter", system-ui, -apple-system, Roboto, "Segoe UI", "Helvetica Neue", "Noto Sans",
 | 
			
		||||
        Oxygen, Ubuntu, Cantarell, "Open Sans", Arial, sans-serif;
 | 
			
		||||
    font-family: var(--font-default-sans);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Force Sonner toast to use Inter first */
 | 
			
		||||
[data-sonner-toast],
 | 
			
		||||
.sonner-toast,
 | 
			
		||||
:where([data-sonner-toast]) :where([data-title]) :where([data-description]) {
 | 
			
		||||
    font-family: var(--font-default-sans) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html,
 | 
			
		||||
body {
 | 
			
		||||
    font-family: var(--font-default-sans);
 | 
			
		||||
}
 | 
			
		||||
@ -1,10 +1,19 @@
 | 
			
		||||
import { StrictMode } from 'react'
 | 
			
		||||
import { createRoot } from 'react-dom/client'
 | 
			
		||||
import './index.css'
 | 
			
		||||
import App from './App.jsx'
 | 
			
		||||
import { StrictMode } from "react";
 | 
			
		||||
import { createRoot } from "react-dom/client";
 | 
			
		||||
import { Toaster } from "sonner";
 | 
			
		||||
import App from "./App.jsx";
 | 
			
		||||
import "./assets/fonts/font-face.css";
 | 
			
		||||
import "./index.css";
 | 
			
		||||
 | 
			
		||||
createRoot(document.getElementById('root')).render(
 | 
			
		||||
createRoot(document.getElementById("root")).render(
 | 
			
		||||
    <StrictMode>
 | 
			
		||||
        <App />
 | 
			
		||||
  </StrictMode>,
 | 
			
		||||
)
 | 
			
		||||
        <Toaster
 | 
			
		||||
            richColors
 | 
			
		||||
            className="flex justify-center"
 | 
			
		||||
            position="bottom-center"
 | 
			
		||||
            duration="7000"
 | 
			
		||||
            theme="dark"
 | 
			
		||||
        />
 | 
			
		||||
    </StrictMode>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,21 @@
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import react from '@vitejs/plugin-react'
 | 
			
		||||
import tailwindcss from '@tailwindcss/vite'
 | 
			
		||||
import tailwindcss from "@tailwindcss/vite";
 | 
			
		||||
import react from "@vitejs/plugin-react-swc";
 | 
			
		||||
import { readFileSync } from "fs";
 | 
			
		||||
import { defineConfig } from "vite";
 | 
			
		||||
 | 
			
		||||
const packageJson = JSON.parse(readFileSync("./package.json", "utf8"));
 | 
			
		||||
 | 
			
		||||
const replaceVersionPlugin = () => {
 | 
			
		||||
    return {
 | 
			
		||||
        name: "replace-version",
 | 
			
		||||
        transformIndexHtml(html) {
 | 
			
		||||
            return html.replace("%APPVERSION%", packageJson.version);
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// https://vite.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  base: './',
 | 
			
		||||
  plugins: [react(), tailwindcss()],
 | 
			
		||||
})
 | 
			
		||||
    base: "./",
 | 
			
		||||
    plugins: [react(), tailwindcss(), replaceVersionPlugin()],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										813
									
								
								inject.js
									
									
									
									
									
								
							
							
						
						
									
										813
									
								
								inject.js
									
									
									
									
									
								
							@ -1,813 +0,0 @@
 | 
			
		||||
let widevineDeviceInfo = null;
 | 
			
		||||
let playreadyDeviceInfo = null;
 | 
			
		||||
let originalChallenge = null
 | 
			
		||||
let serviceCertFound = false;
 | 
			
		||||
let drmType = "NONE";
 | 
			
		||||
let psshFound = false;
 | 
			
		||||
let foundWidevinePssh = null;
 | 
			
		||||
let foundPlayreadyPssh = null;
 | 
			
		||||
let drmDecided = null;
 | 
			
		||||
let drmOverride = "DISABLED";
 | 
			
		||||
let interceptType = "DISABLED";
 | 
			
		||||
let remoteCDM = null;
 | 
			
		||||
let generateRequestCalled = false;
 | 
			
		||||
let remoteListenerMounted = false;
 | 
			
		||||
let injectionSuccess = false;
 | 
			
		||||
let foundChallengeInBody = false;
 | 
			
		||||
let licenseResponseCounter = 0;
 | 
			
		||||
let keysRetrieved = false;
 | 
			
		||||
 | 
			
		||||
// Post message to content.js to get DRM override
 | 
			
		||||
window.postMessage({ type: "__GET_DRM_OVERRIDE__" }, "*");
 | 
			
		||||
 | 
			
		||||
// Add listener for DRM override messages
 | 
			
		||||
window.addEventListener("message", function(event) {
 | 
			
		||||
  if (event.source !== window) return;
 | 
			
		||||
    if (event.data.type === "__DRM_OVERRIDE__") {
 | 
			
		||||
    drmOverride = event.data.drmOverride || "DISABLED";
 | 
			
		||||
    console.log("DRM Override set to:", drmOverride);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Post message to content.js to get injection type
 | 
			
		||||
window.postMessage({ type: "__GET_INJECTION_TYPE__" }, "*");
 | 
			
		||||
 | 
			
		||||
// Add listener for injection type messages
 | 
			
		||||
window.addEventListener("message", function(event) {
 | 
			
		||||
  if (event.source !== window) return;
 | 
			
		||||
 | 
			
		||||
  if (event.data.type === "__INJECTION_TYPE__") {
 | 
			
		||||
    interceptType = event.data.injectionType || "DISABLED";
 | 
			
		||||
    console.log("Injection type set to:", interceptType);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Post message to get CDM devices
 | 
			
		||||
window.postMessage({ type: "__GET_CDM_DEVICES__" }, "*");
 | 
			
		||||
 | 
			
		||||
// Add listener for CDM device messages
 | 
			
		||||
window.addEventListener("message", function(event) {
 | 
			
		||||
  if (event.source !== window) return;
 | 
			
		||||
 | 
			
		||||
  if (event.data.type === "__CDM_DEVICES__") {
 | 
			
		||||
    const { widevine_device, playready_device } = event.data;
 | 
			
		||||
 | 
			
		||||
    console.log("Received device info:", widevine_device, playready_device);
 | 
			
		||||
 | 
			
		||||
    widevineDeviceInfo = widevine_device;
 | 
			
		||||
    playreadyDeviceInfo = playready_device;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// PlayReady Remote CDM Class
 | 
			
		||||
class remotePlayReadyCDM {
 | 
			
		||||
    constructor(security_level, host, secret, device_name) {
 | 
			
		||||
        this.security_level = security_level;
 | 
			
		||||
        this.host = host;
 | 
			
		||||
        this.secret = secret;
 | 
			
		||||
        this.device_name = device_name;
 | 
			
		||||
        this.session_id = null;
 | 
			
		||||
        this.challenge = null;
 | 
			
		||||
        this.keys = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Open PlayReady session
 | 
			
		||||
    openSession() {
 | 
			
		||||
        const url = `${this.host}/remotecdm/playready/${this.device_name}/open`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('GET', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        xhr.send();
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.session_id) {
 | 
			
		||||
            this.session_id = jsonData.data.session_id;
 | 
			
		||||
            console.log("PlayReady session opened:", this.session_id);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to open PlayReady session:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to open PlayReady session");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get PlayReady challenge
 | 
			
		||||
    getChallenge(init_data) {
 | 
			
		||||
        const url = `${this.host}/remotecdm/playready/${this.device_name}/get_license_challenge`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('POST', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        const body = {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            init_data: init_data
 | 
			
		||||
        };
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.challenge) {
 | 
			
		||||
            this.challenge = btoa(jsonData.data.challenge);
 | 
			
		||||
            console.log("PlayReady challenge received:", this.challenge);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to get PlayReady challenge:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to get PlayReady challenge");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Parse PlayReady license response
 | 
			
		||||
    parseLicense(license_message) {
 | 
			
		||||
        const url = `${this.host}/remotecdm/playready/${this.device_name}/parse_license`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('POST', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        const body = {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            license_message: license_message
 | 
			
		||||
        }
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.message === "Successfully parsed and loaded the Keys from the License message")
 | 
			
		||||
        {
 | 
			
		||||
            console.log("PlayReady license response parsed successfully");
 | 
			
		||||
            return true;
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to parse PlayReady license response:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to parse PlayReady license response");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get PlayReady keys
 | 
			
		||||
    getKeys() {
 | 
			
		||||
        const url = `${this.host}/remotecdm/playready/${this.device_name}/get_keys`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('POST', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        const body = {
 | 
			
		||||
            session_id: this.session_id
 | 
			
		||||
        }
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.keys) {
 | 
			
		||||
            this.keys = jsonData.data.keys;
 | 
			
		||||
            console.log("PlayReady keys received:", this.keys);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to get PlayReady keys:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to get PlayReady keys");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Close PlayReady session
 | 
			
		||||
    closeSession () {
 | 
			
		||||
        const url = `${this.host}/remotecdm/playready/${this.device_name}/close/${this.session_id}`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('GET', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        xhr.send();
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData) {
 | 
			
		||||
            console.log("PlayReady session closed successfully");
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to close PlayReady session:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to close PlayReady session");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Widevine Remote CDM Class
 | 
			
		||||
class remoteWidevineCDM {
 | 
			
		||||
        constructor(device_type, system_id, security_level, host, secret, device_name) {
 | 
			
		||||
            this.device_type = device_type;
 | 
			
		||||
            this.system_id = system_id;
 | 
			
		||||
            this.security_level = security_level;
 | 
			
		||||
            this.host = host;
 | 
			
		||||
            this.secret = secret;
 | 
			
		||||
            this.device_name = device_name;
 | 
			
		||||
            this.session_id = null;
 | 
			
		||||
            this.challenge = null;
 | 
			
		||||
            this.keys = null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    // Open Widevine session
 | 
			
		||||
    openSession () {
 | 
			
		||||
        const url = `${this.host}/remotecdm/widevine/${this.device_name}/open`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('GET', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        xhr.send();
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.session_id) {
 | 
			
		||||
            this.session_id = jsonData.data.session_id;
 | 
			
		||||
            console.log("Widevine session opened:", this.session_id);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to open Widevine session:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to open Widevine session");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set Widevine service certificate
 | 
			
		||||
    setServiceCertificate(certificate) {
 | 
			
		||||
        const url = `${this.host}/remotecdm/widevine/${this.device_name}/set_service_certificate`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('POST', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        const body = {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            certificate: certificate ?? null
 | 
			
		||||
        }
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.status === 200) {
 | 
			
		||||
            console.log("Service certificate set successfully");
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to set service certificate:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to set service certificate");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get Widevine challenge
 | 
			
		||||
    getChallenge(init_data, license_type = 'STREAMING') {
 | 
			
		||||
        const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_license_challenge/${license_type}`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('POST', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        const body = {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            init_data: init_data,
 | 
			
		||||
            privacy_mode: serviceCertFound
 | 
			
		||||
        };
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.challenge_b64) {
 | 
			
		||||
            this.challenge = jsonData.data.challenge_b64;
 | 
			
		||||
            console.log("Widevine challenge received:", this.challenge);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to get Widevine challenge:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to get Widevine challenge");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Parse Widevine license response
 | 
			
		||||
    parseLicense(license_message) {
 | 
			
		||||
        const url =  `${this.host}/remotecdm/widevine/${this.device_name}/parse_license`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('POST', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        const body = {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            license_message: license_message
 | 
			
		||||
        };
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.status === 200) {
 | 
			
		||||
            console.log("Widevine license response parsed successfully");
 | 
			
		||||
            return true;
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to parse Widevine license response:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to parse Widevine license response");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get Widevine keys
 | 
			
		||||
    getKeys() {
 | 
			
		||||
        const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_keys/ALL`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('POST', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        const body = {
 | 
			
		||||
            session_id: this.session_id
 | 
			
		||||
        };
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.keys) {
 | 
			
		||||
            this.keys = jsonData.data.keys;
 | 
			
		||||
            console.log("Widevine keys received:", this.keys);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to get Widevine keys:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to get Widevine keys");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Close Widevine session
 | 
			
		||||
    closeSession() {
 | 
			
		||||
        const url = `${this.host}/remotecdm/widevine/${this.device_name}/close/${this.session_id}`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open('GET', url, false);
 | 
			
		||||
        xhr.setRequestHeader('Content-Type', 'application/json');
 | 
			
		||||
        xhr.send();
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData) {
 | 
			
		||||
            console.log("Widevine session closed successfully");
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to close Widevine session:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to close Widevine session");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Utility functions
 | 
			
		||||
function hexStrToU8(hexString) {
 | 
			
		||||
    return Uint8Array.from(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function u8ToHexStr(bytes) {
 | 
			
		||||
    return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function b64ToHexStr(b64) {
 | 
			
		||||
    return [...atob(b64)].map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join``;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function jsonContainsValue(obj, prefix = "CAES") {
 | 
			
		||||
    if (typeof obj === "string") return obj.startsWith(prefix);
 | 
			
		||||
    if (Array.isArray(obj)) return obj.some(val => jsonContainsValue(val, prefix));
 | 
			
		||||
    if (typeof obj === "object" && obj !== null) {
 | 
			
		||||
        return Object.values(obj).some(val => jsonContainsValue(val, prefix));
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function jsonReplaceValue(obj, newValue) {
 | 
			
		||||
    if (typeof obj === "string") {
 | 
			
		||||
        return obj.startsWith("CAES") || obj.startsWith("PD94") ? newValue : obj;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (Array.isArray(obj)) {
 | 
			
		||||
        return obj.map(item => jsonReplaceValue(item, newValue));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (typeof obj === "object" && obj !== null) {
 | 
			
		||||
        const newObj = {};
 | 
			
		||||
        for (const key in obj) {
 | 
			
		||||
            if (Object.hasOwn(obj, key)) {
 | 
			
		||||
                newObj[key] = jsonReplaceValue(obj[key], newValue);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return newObj;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return obj;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isJson(str) {
 | 
			
		||||
    try {
 | 
			
		||||
        JSON.parse(str);
 | 
			
		||||
        return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getWidevinePssh(buffer) {
 | 
			
		||||
    const hex = u8ToHexStr(new Uint8Array(buffer));
 | 
			
		||||
    const match = hex.match(/000000(..)?70737368.*/);
 | 
			
		||||
    if (!match) return null;
 | 
			
		||||
 | 
			
		||||
    const boxHex = match[0];
 | 
			
		||||
    const bytes = hexStrToU8(boxHex);
 | 
			
		||||
    return window.btoa(String.fromCharCode(...bytes));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getPlayReadyPssh(buffer) {
 | 
			
		||||
    const u8 = new Uint8Array(buffer);
 | 
			
		||||
    const systemId = "9a04f07998404286ab92e65be0885f95";
 | 
			
		||||
    const hex = u8ToHexStr(u8);
 | 
			
		||||
    const index = hex.indexOf(systemId);
 | 
			
		||||
    if (index === -1) return null;
 | 
			
		||||
    const psshBoxStart = hex.lastIndexOf("70737368", index);
 | 
			
		||||
    if (psshBoxStart === -1) return null;
 | 
			
		||||
    const lenStart = psshBoxStart - 8;
 | 
			
		||||
    const boxLen = parseInt(hex.substr(lenStart, 8), 16) * 2;
 | 
			
		||||
    const psshHex = hex.substr(lenStart, boxLen);
 | 
			
		||||
    const psshBytes = hexStrToU8(psshHex);
 | 
			
		||||
    return window.btoa(String.fromCharCode(...psshBytes));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getClearkey(response) {
 | 
			
		||||
    let obj = JSON.parse((new TextDecoder("utf-8")).decode(response));
 | 
			
		||||
    return obj["keys"].map(o => ({
 | 
			
		||||
        key_id: b64ToHexStr(o["kid"].replace(/-/g, '+').replace(/_/g, '/')),
 | 
			
		||||
        key: b64ToHexStr(o["k"].replace(/-/g, '+').replace(/_/g, '/')),
 | 
			
		||||
    }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function base64ToUint8Array(base64) {
 | 
			
		||||
    const binaryStr = atob(base64);
 | 
			
		||||
    const len = binaryStr.length;
 | 
			
		||||
    const bytes = new Uint8Array(len);
 | 
			
		||||
    for (let i = 0; i < len; i++) {
 | 
			
		||||
        bytes[i] = binaryStr.charCodeAt(i);
 | 
			
		||||
    }
 | 
			
		||||
    return bytes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function arrayBufferToBase64(uint8array) {
 | 
			
		||||
    let binary = '';
 | 
			
		||||
    const len = uint8array.length;
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < len; i++) {
 | 
			
		||||
        binary += String.fromCharCode(uint8array[i]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return window.btoa(binary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Challenge generator interceptor
 | 
			
		||||
const originalGenerateRequest = MediaKeySession.prototype.generateRequest;
 | 
			
		||||
MediaKeySession.prototype.generateRequest = function(initDataType, initData) {
 | 
			
		||||
    const session = this;
 | 
			
		||||
    let playReadyPssh = getPlayReadyPssh(initData);
 | 
			
		||||
    if (playReadyPssh) {
 | 
			
		||||
        console.log("[DRM Detected] PlayReady");
 | 
			
		||||
        foundPlayreadyPssh = playReadyPssh;
 | 
			
		||||
        console.log("[PlayReady PSSH found] " + playReadyPssh)
 | 
			
		||||
    }
 | 
			
		||||
    let wideVinePssh = getWidevinePssh(initData)
 | 
			
		||||
    if (wideVinePssh) {
 | 
			
		||||
        // Widevine code
 | 
			
		||||
        console.log("[DRM Detected] Widevine");
 | 
			
		||||
        foundWidevinePssh = wideVinePssh;
 | 
			
		||||
        console.log("[Widevine PSSH found] " + wideVinePssh)
 | 
			
		||||
    }
 | 
			
		||||
    // Challenge message interceptor
 | 
			
		||||
    if (!remoteListenerMounted) {
 | 
			
		||||
        remoteListenerMounted = true;
 | 
			
		||||
        session.addEventListener("message", function messageInterceptor(event) {
 | 
			
		||||
            event.stopImmediatePropagation();
 | 
			
		||||
            const uint8Array = new Uint8Array(event.message);
 | 
			
		||||
            const base64challenge = arrayBufferToBase64(uint8Array);
 | 
			
		||||
            if (base64challenge === "CAQ=" && interceptType !== "DISABLED" && !serviceCertFound) {
 | 
			
		||||
                const {
 | 
			
		||||
                    device_type, system_id, security_level, host, secret, device_name
 | 
			
		||||
                } = widevineDeviceInfo;
 | 
			
		||||
                remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
 | 
			
		||||
                remoteCDM.openSession();
 | 
			
		||||
            }
 | 
			
		||||
            if (!injectionSuccess && base64challenge !== "CAQ=" && interceptType !== "DISABLED") {
 | 
			
		||||
                if (interceptType === "EME") {
 | 
			
		||||
                    injectionSuccess = true;
 | 
			
		||||
                }
 | 
			
		||||
                if (!originalChallenge) {
 | 
			
		||||
                    originalChallenge = base64challenge;
 | 
			
		||||
                }
 | 
			
		||||
                if (originalChallenge.startsWith("CAES")) {
 | 
			
		||||
                    window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*");
 | 
			
		||||
                    window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*");
 | 
			
		||||
                    if (interceptType === "EME" && !remoteCDM) {
 | 
			
		||||
                        const {
 | 
			
		||||
                            device_type, system_id, security_level, host, secret, device_name
 | 
			
		||||
                        } = widevineDeviceInfo;
 | 
			
		||||
                        remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
 | 
			
		||||
                        remoteCDM.openSession();
 | 
			
		||||
                        remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                    }}
 | 
			
		||||
                if (!originalChallenge.startsWith("CAES")) {
 | 
			
		||||
                    const buffer = event.message;
 | 
			
		||||
                    const decoder = new TextDecoder('utf-16');
 | 
			
		||||
                    const decodedText = decoder.decode(buffer);
 | 
			
		||||
                    const match = decodedText.match(/<Challenge encoding="base64encoded">([^<]+)<\/Challenge>/);
 | 
			
		||||
                    if (match) {
 | 
			
		||||
                        window.postMessage({ type: "__DRM_TYPE__", data: "PlayReady" }, "*");
 | 
			
		||||
                        window.postMessage({ type: "__PSSH_DATA__", data: foundPlayreadyPssh }, "*");
 | 
			
		||||
                        originalChallenge = match[1];
 | 
			
		||||
                        if (interceptType === "EME" && !remoteCDM) {    
 | 
			
		||||
                            const {
 | 
			
		||||
                                security_level, host, secret, device_name
 | 
			
		||||
                            } = playreadyDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name)
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundPlayreadyPssh);
 | 
			
		||||
                        }
 | 
			
		||||
                    }}
 | 
			
		||||
                if (interceptType === "EME" && remoteCDM) {
 | 
			
		||||
                    const uint8challenge = base64ToUint8Array(remoteCDM.challenge);
 | 
			
		||||
                    const challengeBuffer = uint8challenge.buffer;
 | 
			
		||||
                    const syntheticEvent = new MessageEvent("message", {
 | 
			
		||||
                        data: event.data,
 | 
			
		||||
                        origin: event.origin,
 | 
			
		||||
                        lastEventId: event.lastEventId,
 | 
			
		||||
                        source: event.source,
 | 
			
		||||
                        ports: event.ports
 | 
			
		||||
                    });
 | 
			
		||||
                    Object.defineProperty(syntheticEvent, "message", {
 | 
			
		||||
                        get: () => challengeBuffer
 | 
			
		||||
                    });
 | 
			
		||||
                    console.log("Intercepted EME Challenge and injected custom one.")
 | 
			
		||||
                    session.dispatchEvent(syntheticEvent);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        console.log("Message interceptor mounted.");
 | 
			
		||||
    }
 | 
			
		||||
return originalGenerateRequest.call(session, initDataType, initData);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Message update interceptors
 | 
			
		||||
const originalUpdate = MediaKeySession.prototype.update;
 | 
			
		||||
MediaKeySession.prototype.update = function(response) {
 | 
			
		||||
    const uint8 = response instanceof Uint8Array ? response : new Uint8Array(response);
 | 
			
		||||
    const base64Response = window.btoa(String.fromCharCode(...uint8));
 | 
			
		||||
    if (base64Response.startsWith("CAUS") && foundWidevinePssh && remoteCDM && !serviceCertFound) {
 | 
			
		||||
        remoteCDM.setServiceCertificate(base64Response);
 | 
			
		||||
        if (interceptType === "EME" && !remoteCDM.challenge) {
 | 
			
		||||
            remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
        }
 | 
			
		||||
        window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*");
 | 
			
		||||
        window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*");
 | 
			
		||||
        serviceCertFound = true;
 | 
			
		||||
    }
 | 
			
		||||
    if (!base64Response.startsWith("CAUS") && (foundWidevinePssh || foundPlayreadyPssh) && !keysRetrieved) {
 | 
			
		||||
        if (licenseResponseCounter === 1 || foundChallengeInBody) {
 | 
			
		||||
            remoteCDM.parseLicense(base64Response);
 | 
			
		||||
            remoteCDM.getKeys();
 | 
			
		||||
            remoteCDM.closeSession();
 | 
			
		||||
            keysRetrieved = true;
 | 
			
		||||
            window.postMessage({ type: "__KEYS_DATA__", data: remoteCDM.keys }, "*");
 | 
			
		||||
        }
 | 
			
		||||
        licenseResponseCounter++;
 | 
			
		||||
    }
 | 
			
		||||
    const updatePromise = originalUpdate.call(this, response);
 | 
			
		||||
    if (!foundPlayreadyPssh && !foundWidevinePssh) {
 | 
			
		||||
        updatePromise
 | 
			
		||||
            .then(() => {
 | 
			
		||||
                let clearKeys = getClearkey(response);
 | 
			
		||||
                if (clearKeys && clearKeys.length > 0) {
 | 
			
		||||
                  console.log("[CLEARKEY] ", clearKeys);
 | 
			
		||||
                  const drmType = {
 | 
			
		||||
                      type: "__DRM_TYPE__",
 | 
			
		||||
                      data: 'ClearKey'
 | 
			
		||||
                  };
 | 
			
		||||
                  window.postMessage(drmType, "*");
 | 
			
		||||
                  const keysData = {
 | 
			
		||||
                      type: "__KEYS_DATA__",
 | 
			
		||||
                      data: clearKeys
 | 
			
		||||
                  };
 | 
			
		||||
                  window.postMessage(keysData, "*");
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .catch(e => {
 | 
			
		||||
                console.log("[CLEARKEY] Not found");
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return updatePromise;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// fetch POST interceptor
 | 
			
		||||
(function() {
 | 
			
		||||
  const originalFetch = window.fetch;
 | 
			
		||||
 | 
			
		||||
  window.fetch = async function(resource, config = {}) {
 | 
			
		||||
    const method = (config.method || 'GET').toUpperCase();
 | 
			
		||||
 | 
			
		||||
    if (method === 'POST') {
 | 
			
		||||
        let body = config.body;
 | 
			
		||||
        if (body) {
 | 
			
		||||
            if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
 | 
			
		||||
                const buffer = body instanceof Uint8Array ? body : new Uint8Array(body);
 | 
			
		||||
                const base64Body = window.btoa(String.fromCharCode(...buffer));
 | 
			
		||||
                if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64Body !== remoteCDM.challenge) && interceptType === "EME") {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
 | 
			
		||||
                    // Block the request
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && interceptType == "LICENSE" &&!foundChallengeInBody) {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
 | 
			
		||||
                    if (!remoteCDM) {
 | 
			
		||||
                        if (base64Body.startsWith("CAES")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                device_type, system_id, security_level, host, secret, device_name
 | 
			
		||||
                            } = widevineDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (base64Body.startsWith("PD94")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                security_level, host, secret, device_name
 | 
			
		||||
                            } = playreadyDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundPlayreadyPssh);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (remoteCDM && remoteCDM.challenge === null) {
 | 
			
		||||
                        remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                    }
 | 
			
		||||
                    const injectedBody = base64ToUint8Array(remoteCDM.challenge);
 | 
			
		||||
                    config.body = injectedBody;
 | 
			
		||||
                    return originalFetch(resource, config);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (typeof body === 'string' && !isJson(body)) {
 | 
			
		||||
                const base64EncodedBody = btoa(body);
 | 
			
		||||
                if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64EncodedBody !== remoteCDM.challenge) && interceptType === "EME") {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
 | 
			
		||||
                    // Block the request
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && interceptType == "LICENSE" && !foundChallengeInBody) {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
 | 
			
		||||
                    if (!remoteCDM) {
 | 
			
		||||
                        if (base64EncodedBody.startsWith("CAES")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                device_type, system_id, security_level, host, secret, device_name
 | 
			
		||||
                            } = widevineDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (base64EncodedBody.startsWith("PD94")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                security_level, host, secret, device_name
 | 
			
		||||
                            } = playreadyDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundPlayreadyPssh);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (remoteCDM && remoteCDM.challenge === null) {
 | 
			
		||||
                        remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                    }
 | 
			
		||||
                    const injectedBody = atob(remoteCDM.challenge);
 | 
			
		||||
                    config.body = injectedBody;
 | 
			
		||||
                    return originalFetch(resource, config);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (typeof body === 'string' && isJson(body)) {
 | 
			
		||||
                const jsonBody = JSON.parse(body);
 | 
			
		||||
 | 
			
		||||
                if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && (!remoteCDM || remoteCDM.challenge === null) && interceptType === "EME") {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
 | 
			
		||||
                    // Block the request
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && interceptType === "LICENSE" && !foundChallengeInBody) {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: resource }, "*");
 | 
			
		||||
                    if (!remoteCDM) {
 | 
			
		||||
                        if (jsonContainsValue(jsonBody, "CAES")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                device_type, system_id, security_level, host, secret, device_name
 | 
			
		||||
                            } = widevineDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (jsonContainsValue(jsonBody, "PD94")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                security_level, host, secret, device_name
 | 
			
		||||
                            } = playreadyDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundPlayreadyPssh);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (remoteCDM && remoteCDM.challenge === null) {
 | 
			
		||||
                        remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                    }
 | 
			
		||||
                    const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
 | 
			
		||||
                    config.body = JSON.stringify(injectedBody);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return originalFetch(resource, config);
 | 
			
		||||
  };
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
// XHR POST interceptor
 | 
			
		||||
(function() {
 | 
			
		||||
  const originalOpen = XMLHttpRequest.prototype.open;
 | 
			
		||||
  const originalSend = XMLHttpRequest.prototype.send;
 | 
			
		||||
 | 
			
		||||
  XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
 | 
			
		||||
    this._method = method;
 | 
			
		||||
    this._url = url;
 | 
			
		||||
    return originalOpen.apply(this, arguments);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  XMLHttpRequest.prototype.send = function(body) {
 | 
			
		||||
    if (this._method && this._method.toUpperCase() === 'POST') {
 | 
			
		||||
        if (body) {
 | 
			
		||||
 | 
			
		||||
            if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
 | 
			
		||||
                const buffer = body instanceof Uint8Array ? body : new Uint8Array(body);
 | 
			
		||||
                const base64Body = window.btoa(String.fromCharCode(...buffer));
 | 
			
		||||
                if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64Body !== remoteCDM.challenge) && interceptType === "EME") {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
 | 
			
		||||
                    // Block the request
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if ((base64Body.startsWith("CAES") || base64Body.startsWith("PD94")) && interceptType == "LICENSE" &&!foundChallengeInBody) {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
 | 
			
		||||
                    if (!remoteCDM) {
 | 
			
		||||
                        if (base64Body.startsWith("CAES")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                device_type, system_id, security_level, host, secret, device_name
 | 
			
		||||
                            } = widevineDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (base64Body.startsWith("PD94")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                security_level, host, secret, device_name
 | 
			
		||||
                            } = playreadyDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundPlayreadyPssh);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (remoteCDM && remoteCDM.challenge === null) {
 | 
			
		||||
                        remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                    }
 | 
			
		||||
                    const injectedBody = base64ToUint8Array(remoteCDM.challenge);
 | 
			
		||||
                    return originalSend.call(this, injectedBody);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (typeof body === 'string' && !isJson(body)) {
 | 
			
		||||
                const base64EncodedBody = btoa(body);
 | 
			
		||||
                if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && (!remoteCDM || remoteCDM.challenge === null || base64EncodedBody !== remoteCDM.challenge) && interceptType === "EME") {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
 | 
			
		||||
                    // Block the request
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if ((base64EncodedBody.startsWith("CAES") || base64EncodedBody.startsWith("PD94")) && interceptType == "LICENSE" && !foundChallengeInBody) {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
 | 
			
		||||
                    if (!remoteCDM) {
 | 
			
		||||
                        if (base64EncodedBody.startsWith("CAES")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                device_type, system_id, security_level, host, secret, device_name
 | 
			
		||||
                            } = widevineDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (base64EncodedBody.startsWith("PD94")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                security_level, host, secret, device_name
 | 
			
		||||
                            } = playreadyDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundPlayreadyPssh);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (remoteCDM && remoteCDM.challenge === null) {
 | 
			
		||||
                        remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                    }
 | 
			
		||||
                    const injectedBody = atob(remoteCDM.challenge);
 | 
			
		||||
                    return originalSend.call(this, injectedBody);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (typeof body === 'string' && isJson(body)) {
 | 
			
		||||
                const jsonBody = JSON.parse(body);
 | 
			
		||||
 | 
			
		||||
                if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && (!remoteCDM || remoteCDM.challenge === null) && interceptType === "EME") {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
 | 
			
		||||
                    // Block the request
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if ((jsonContainsValue(jsonBody, "CAES") || jsonContainsValue(jsonBody, "PD94")) && interceptType === "LICENSE" && !foundChallengeInBody) {
 | 
			
		||||
                    foundChallengeInBody = true;
 | 
			
		||||
                    window.postMessage({ type: "__LICENSE_URL__", data: this._url }, "*");
 | 
			
		||||
                    if (!remoteCDM) {
 | 
			
		||||
                        if (jsonContainsValue(jsonBody, "CAES")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                device_type, system_id, security_level, host, secret, device_name
 | 
			
		||||
                            } = widevineDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remoteWidevineCDM(device_type, system_id, security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (jsonContainsValue(jsonBody, "PD94")) {
 | 
			
		||||
                            const {
 | 
			
		||||
                                security_level, host, secret, device_name
 | 
			
		||||
                            } = playreadyDeviceInfo;
 | 
			
		||||
                            remoteCDM = new remotePlayReadyCDM(security_level, host, secret, device_name);
 | 
			
		||||
                            remoteCDM.openSession();
 | 
			
		||||
                            remoteCDM.getChallenge(foundPlayreadyPssh);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    if (remoteCDM && remoteCDM.challenge === null) {
 | 
			
		||||
                        remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
                    }
 | 
			
		||||
                    const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
 | 
			
		||||
                    return originalSend.call(this, JSON.stringify(injectedBody));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return originalSend.apply(this, arguments);
 | 
			
		||||
  };
 | 
			
		||||
})();
 | 
			
		||||
@ -1,41 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "manifest_version": 2,
 | 
			
		||||
  "name": "CDRM Extension 2.0",
 | 
			
		||||
  "version": "2.0",
 | 
			
		||||
  "description": "Decrypt DRM Protected content",
 | 
			
		||||
  "permissions": [
 | 
			
		||||
    "webRequest",
 | 
			
		||||
    "webRequestBlocking",
 | 
			
		||||
    "<all_urls>",
 | 
			
		||||
    "activeTab",
 | 
			
		||||
    "storage",
 | 
			
		||||
    "tabs",
 | 
			
		||||
    "contextMenus"
 | 
			
		||||
  ],
 | 
			
		||||
  "background": {
 | 
			
		||||
    "scripts": ["background.js"],
 | 
			
		||||
    "persistent": true
 | 
			
		||||
  },
 | 
			
		||||
  "content_scripts": [
 | 
			
		||||
    {
 | 
			
		||||
      "matches": ["<all_urls>"],
 | 
			
		||||
      "js": ["content.js"],
 | 
			
		||||
      "run_at": "document_start",
 | 
			
		||||
      "all_frames": true
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "web_accessible_resources": ["inject.js"],
 | 
			
		||||
  "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
 | 
			
		||||
  "browser_action": {
 | 
			
		||||
    "default_icon": {
 | 
			
		||||
      "16": "icons/icon16.png",
 | 
			
		||||
      "32": "icons/icon32.png",
 | 
			
		||||
      "128": "icons/icon128.png"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "icons": {
 | 
			
		||||
    "16": "icons/icon16.png",
 | 
			
		||||
    "32": "icons/icon32.png",
 | 
			
		||||
    "128": "icons/icon128.png"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								mv2/background.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								mv2/background.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,89 @@
 | 
			
		||||
// Open popout window when the extension icon is clicked
 | 
			
		||||
chrome.browserAction.onClicked.addListener(() => {
 | 
			
		||||
    chrome.windows.create({
 | 
			
		||||
        url: chrome.runtime.getURL("index.html"),
 | 
			
		||||
        type: "popup", // opens as a floating window
 | 
			
		||||
        width: 800,
 | 
			
		||||
        height: 600,
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Listen for messages and store data in chrome.storage.local
 | 
			
		||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
 | 
			
		||||
    const { type, data } = message;
 | 
			
		||||
 | 
			
		||||
    switch (type) {
 | 
			
		||||
        case "DRM_TYPE":
 | 
			
		||||
            console.log("DRM Type:", data);
 | 
			
		||||
            chrome.storage.local.set({ drmType: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "PSSH_DATA":
 | 
			
		||||
            console.log("Storing PSSH:", data);
 | 
			
		||||
            chrome.storage.local.set({ latestPSSH: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "KEYS_DATA":
 | 
			
		||||
            console.log("Storing Decryption Keys:", data);
 | 
			
		||||
            chrome.storage.local.set({ latestKeys: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "LICENSE_URL":
 | 
			
		||||
            console.log("Storling License URL " + data);
 | 
			
		||||
            chrome.storage.local.set({ licenseURL: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "MANIFEST_URL_FOUND":
 | 
			
		||||
            console.log("Storing Manifest URL:", data);
 | 
			
		||||
            chrome.storage.local.set({ manifestURL: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
            console.warn("Unknown message type received:", type);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Set initial config and injection type on install
 | 
			
		||||
chrome.runtime.onInstalled.addListener((details) => {
 | 
			
		||||
    if (details.reason === "install") {
 | 
			
		||||
        chrome.storage.local.set({ valid_config: false }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("Error setting valid_config:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("valid_config set to false on first install.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("Error setting Injection Type:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("Injection type set to LICENSE on first install.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        chrome.storage.local.set({ drm_override: "DISABLED" }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("Error setting DRM Override type:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("DRM Override type set to DISABLED on first install.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        chrome.storage.local.set({ cdrm_instance: null }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("Error setting CDRM instance:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("CDRM instance set to null.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        chrome.storage.local.set({ cdrm_api_key: null }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("Error setting CDRM API Key:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("CDRM API Key set.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										96
									
								
								mv2/content.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								mv2/content.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
			
		||||
// Inject `inject.js` into the page context
 | 
			
		||||
(function injectScript() {
 | 
			
		||||
    const script = document.createElement("script");
 | 
			
		||||
    script.src = chrome.runtime.getURL("inject.js");
 | 
			
		||||
    script.type = "text/javascript";
 | 
			
		||||
    script.onload = () => script.remove(); // Clean up
 | 
			
		||||
    // Inject directly into <html> or <head>
 | 
			
		||||
    (document.documentElement || document.head || document.body).appendChild(script);
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
// Listen for messages from the injected script
 | 
			
		||||
window.addEventListener("message", function (event) {
 | 
			
		||||
    if (event.source !== window) return;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        ["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes(
 | 
			
		||||
            event.data?.type
 | 
			
		||||
        )
 | 
			
		||||
    ) {
 | 
			
		||||
        chrome.runtime.sendMessage({
 | 
			
		||||
            type: event.data.type.replace("__", "").replace("__", ""),
 | 
			
		||||
            data: event.data.data,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data.type === "__GET_CDM_DEVICES__") {
 | 
			
		||||
        chrome.storage.local.get(["widevine_device", "playready_device"], (result) => {
 | 
			
		||||
            const widevine_device = result.widevine_device || null;
 | 
			
		||||
            const playready_device = result.playready_device || null;
 | 
			
		||||
 | 
			
		||||
            window.postMessage(
 | 
			
		||||
                {
 | 
			
		||||
                    type: "__CDM_DEVICES__",
 | 
			
		||||
                    widevine_device,
 | 
			
		||||
                    playready_device,
 | 
			
		||||
                },
 | 
			
		||||
                "*"
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data.type === "__GET_INJECTION_TYPE__") {
 | 
			
		||||
        chrome.storage.local.get("injection_type", (result) => {
 | 
			
		||||
            const injectionType = result.injection_type || "LICENSE";
 | 
			
		||||
 | 
			
		||||
            window.postMessage(
 | 
			
		||||
                {
 | 
			
		||||
                    type: "__INJECTION_TYPE__",
 | 
			
		||||
                    injectionType,
 | 
			
		||||
                },
 | 
			
		||||
                "*"
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data.type === "__GET_DRM_OVERRIDE__") {
 | 
			
		||||
        chrome.storage.local.get("drm_override", (result) => {
 | 
			
		||||
            const drmOverride = result.drm_override || "DISABLED";
 | 
			
		||||
 | 
			
		||||
            window.postMessage(
 | 
			
		||||
                {
 | 
			
		||||
                    type: "__DRM_OVERRIDE__",
 | 
			
		||||
                    drmOverride,
 | 
			
		||||
                },
 | 
			
		||||
                "*"
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Manifest header and URL
 | 
			
		||||
 | 
			
		||||
    const seenManifestUrls = new Set();
 | 
			
		||||
 | 
			
		||||
    if (event.data?.type === "__MANIFEST_URL__") {
 | 
			
		||||
        const url = event.data.data;
 | 
			
		||||
        if (seenManifestUrls.has(url)) return;
 | 
			
		||||
        seenManifestUrls.add(url);
 | 
			
		||||
        console.log("✅ [Content] Unique manifest URL:", url);
 | 
			
		||||
 | 
			
		||||
        chrome.runtime.sendMessage({
 | 
			
		||||
            type: "MANIFEST_URL_FOUND",
 | 
			
		||||
            data: url,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data?.type === "__MANIFEST_HEADERS__") {
 | 
			
		||||
        const { url, headers } = event.data;
 | 
			
		||||
        console.log("[Content.js] Manifest Headers:", url, headers);
 | 
			
		||||
 | 
			
		||||
        chrome.runtime.sendMessage({
 | 
			
		||||
            type: "MANIFEST_HEADERS",
 | 
			
		||||
            url,
 | 
			
		||||
            headers,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										1143
									
								
								mv2/inject.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1143
									
								
								mv2/inject.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										41
									
								
								mv2/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								mv2/manifest.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
{
 | 
			
		||||
    "manifest_version": 2,
 | 
			
		||||
    "name": "CDRM Extension",
 | 
			
		||||
    "version": "2.1.0",
 | 
			
		||||
    "description": "Decrypt DRM protected content",
 | 
			
		||||
    "permissions": [
 | 
			
		||||
        "webRequest",
 | 
			
		||||
        "webRequestBlocking",
 | 
			
		||||
        "<all_urls>",
 | 
			
		||||
        "activeTab",
 | 
			
		||||
        "storage",
 | 
			
		||||
        "tabs",
 | 
			
		||||
        "contextMenus"
 | 
			
		||||
    ],
 | 
			
		||||
    "background": {
 | 
			
		||||
        "scripts": ["background.js"],
 | 
			
		||||
        "persistent": true
 | 
			
		||||
    },
 | 
			
		||||
    "content_scripts": [
 | 
			
		||||
        {
 | 
			
		||||
            "matches": ["<all_urls>"],
 | 
			
		||||
            "js": ["content.js"],
 | 
			
		||||
            "run_at": "document_start",
 | 
			
		||||
            "all_frames": true
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "web_accessible_resources": ["inject.js"],
 | 
			
		||||
    "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
 | 
			
		||||
    "browser_action": {
 | 
			
		||||
        "default_icon": {
 | 
			
		||||
            "16": "icons/icon16.png",
 | 
			
		||||
            "32": "icons/icon32.png",
 | 
			
		||||
            "128": "icons/icon128.png"
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "icons": {
 | 
			
		||||
        "16": "icons/icon16.png",
 | 
			
		||||
        "32": "icons/icon32.png",
 | 
			
		||||
        "128": "icons/icon128.png"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										136
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							@ -0,0 +1,136 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "cdrm-extension",
 | 
			
		||||
    "version": "2.1.0",
 | 
			
		||||
    "lockfileVersion": 3,
 | 
			
		||||
    "requires": true,
 | 
			
		||||
    "packages": {
 | 
			
		||||
        "": {
 | 
			
		||||
            "name": "cdrm-extension",
 | 
			
		||||
            "version": "2.1.0",
 | 
			
		||||
            "license": "ISC",
 | 
			
		||||
            "devDependencies": {
 | 
			
		||||
                "terser": "^5.43.1"
 | 
			
		||||
            },
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">=21.0.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@jridgewell/gen-mapping": {
 | 
			
		||||
            "version": "0.3.12",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
 | 
			
		||||
            "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@jridgewell/sourcemap-codec": "^1.5.0",
 | 
			
		||||
                "@jridgewell/trace-mapping": "^0.3.24"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@jridgewell/resolve-uri": {
 | 
			
		||||
            "version": "3.1.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
 | 
			
		||||
            "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">=6.0.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@jridgewell/source-map": {
 | 
			
		||||
            "version": "0.3.10",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz",
 | 
			
		||||
            "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@jridgewell/gen-mapping": "^0.3.5",
 | 
			
		||||
                "@jridgewell/trace-mapping": "^0.3.25"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@jridgewell/sourcemap-codec": {
 | 
			
		||||
            "version": "1.5.4",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
 | 
			
		||||
            "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT"
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/@jridgewell/trace-mapping": {
 | 
			
		||||
            "version": "0.3.29",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz",
 | 
			
		||||
            "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@jridgewell/resolve-uri": "^3.1.0",
 | 
			
		||||
                "@jridgewell/sourcemap-codec": "^1.4.14"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/acorn": {
 | 
			
		||||
            "version": "8.15.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
 | 
			
		||||
            "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "bin": {
 | 
			
		||||
                "acorn": "bin/acorn"
 | 
			
		||||
            },
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">=0.4.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/buffer-from": {
 | 
			
		||||
            "version": "1.1.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
 | 
			
		||||
            "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT"
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/commander": {
 | 
			
		||||
            "version": "2.20.3",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
 | 
			
		||||
            "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT"
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/source-map": {
 | 
			
		||||
            "version": "0.6.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
 | 
			
		||||
            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "BSD-3-Clause",
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">=0.10.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/source-map-support": {
 | 
			
		||||
            "version": "0.5.21",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
 | 
			
		||||
            "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "MIT",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "buffer-from": "^1.0.0",
 | 
			
		||||
                "source-map": "^0.6.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "node_modules/terser": {
 | 
			
		||||
            "version": "5.43.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
 | 
			
		||||
            "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "license": "BSD-2-Clause",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@jridgewell/source-map": "^0.3.3",
 | 
			
		||||
                "acorn": "^8.14.0",
 | 
			
		||||
                "commander": "^2.20.0",
 | 
			
		||||
                "source-map-support": "~0.5.20"
 | 
			
		||||
            },
 | 
			
		||||
            "bin": {
 | 
			
		||||
                "terser": "bin/terser"
 | 
			
		||||
            },
 | 
			
		||||
            "engines": {
 | 
			
		||||
                "node": ">=10"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "cdrm-extension",
 | 
			
		||||
    "version": "3.0",
 | 
			
		||||
    "description": "",
 | 
			
		||||
    "main": "background.js",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "buildext": "node buildext.js",
 | 
			
		||||
        "test": "echo \"Error: no test specified\" && exit 1"
 | 
			
		||||
    },
 | 
			
		||||
    "repository": {
 | 
			
		||||
        "type": "git",
 | 
			
		||||
        "url": "https://cdm-project.com/tpd94/CDRM-Extension.git"
 | 
			
		||||
    },
 | 
			
		||||
    "keywords": [],
 | 
			
		||||
    "author": "",
 | 
			
		||||
    "license": "ISC",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "engines": {
 | 
			
		||||
        "node": ">=21.0.0"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "terser": "^5.43.1"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,13 +0,0 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>CDRM Decryption Extension</title>
 | 
			
		||||
    <script type="module" crossorigin src="./assets/index-ydPQKJSy.js"></script>
 | 
			
		||||
    <link rel="stylesheet" crossorigin href="./assets/index-UaipKa9p.css">
 | 
			
		||||
  </head>
 | 
			
		||||
  <body class="min-w-full min-h-full w-full h-full">
 | 
			
		||||
    <div class="min-w-full min-h-full w-full h-full" id="root"></div>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										88
									
								
								src/background.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/background.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
			
		||||
chrome.action.onClicked.addListener(() => {
 | 
			
		||||
    chrome.windows.create({
 | 
			
		||||
        url: chrome.runtime.getURL("index.html"),
 | 
			
		||||
        type: "popup",
 | 
			
		||||
        width: 800,
 | 
			
		||||
        height: 600,
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Listen for messages and store data in chrome.storage.local
 | 
			
		||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
 | 
			
		||||
    const { type, data } = message;
 | 
			
		||||
 | 
			
		||||
    switch (type) {
 | 
			
		||||
        case "DRM_TYPE":
 | 
			
		||||
            console.log("[CDRM-Extension] DRM Type:", data);
 | 
			
		||||
            chrome.storage.local.set({ drmType: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "PSSH_DATA":
 | 
			
		||||
            console.log("[CDRM-Extension] Storing PSSH:", data);
 | 
			
		||||
            chrome.storage.local.set({ latestPSSH: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "KEYS_DATA":
 | 
			
		||||
            console.log("[CDRM-Extension] Storing Decryption Keys:", data);
 | 
			
		||||
            chrome.storage.local.set({ latestKeys: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "LICENSE_URL":
 | 
			
		||||
            console.log("[CDRM-Extension] Storing License URL " + data);
 | 
			
		||||
            chrome.storage.local.set({ licenseURL: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        case "MANIFEST_URL":
 | 
			
		||||
            console.log("[CDRM-Extension] Storing Manifest URL:", data);
 | 
			
		||||
            chrome.storage.local.set({ manifestURL: data });
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
            console.warn("[CDRM-Extension] Unknown message type received:", type);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Set initial config and injection type on install
 | 
			
		||||
chrome.runtime.onInstalled.addListener((details) => {
 | 
			
		||||
    if (details.reason === "install") {
 | 
			
		||||
        chrome.storage.local.set({ valid_config: false }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("[CDRM-Extension] Error setting valid_config:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("[CDRM-Extension] valid_config set to false on first install.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        chrome.storage.local.set({ injection_type: "LICENSE" }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("[CDRM-Extension] Error setting Injection Type:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("[CDRM-Extension] Injection type set to LICENSE on first install.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        chrome.storage.local.set({ drm_override: "DISABLED" }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("[CDRM-Extension] Error setting DRM Override type:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("[CDRM-Extension] DRM Override type set to DISABLED on first install.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        chrome.storage.local.set({ cdrm_instance: null }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("[CDRM-Extension] Error setting CDRM instance:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("[CDRM-Extension] CDRM instance set to null.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        chrome.storage.local.set({ cdrm_api_key: null }, () => {
 | 
			
		||||
            if (chrome.runtime.lastError) {
 | 
			
		||||
                console.error("[CDRM-Extension] Error setting CDRM API Key:", chrome.runtime.lastError);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log("[CDRM-Extension] CDRM API Key set.");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										96
									
								
								src/content.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/content.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
			
		||||
// Inject `inject.js` into the page context
 | 
			
		||||
(function injectScript() {
 | 
			
		||||
    const script = document.createElement("script");
 | 
			
		||||
    script.src = chrome.runtime.getURL("inject.js");
 | 
			
		||||
    script.type = "text/javascript";
 | 
			
		||||
    script.onload = () => script.remove(); // Clean up
 | 
			
		||||
    // Inject directly into <html> or <head>
 | 
			
		||||
    (document.documentElement || document.head || document.body).appendChild(script);
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
// Listen for messages from the injected script
 | 
			
		||||
window.addEventListener("message", function (event) {
 | 
			
		||||
    if (event.source !== window) return;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        ["__DRM_TYPE__", "__PSSH_DATA__", "__KEYS_DATA__", "__LICENSE_URL__"].includes(
 | 
			
		||||
            event.data?.type
 | 
			
		||||
        )
 | 
			
		||||
    ) {
 | 
			
		||||
        chrome.runtime.sendMessage({
 | 
			
		||||
            type: event.data.type.replace("__", "").replace("__", ""),
 | 
			
		||||
            data: event.data.data,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data.type === "__GET_CDM_DEVICES__") {
 | 
			
		||||
        chrome.storage.local.get(["widevine_device", "playready_device"], (result) => {
 | 
			
		||||
            const widevine_device = result.widevine_device || null;
 | 
			
		||||
            const playready_device = result.playready_device || null;
 | 
			
		||||
 | 
			
		||||
            window.postMessage(
 | 
			
		||||
                {
 | 
			
		||||
                    type: "__CDM_DEVICES__",
 | 
			
		||||
                    widevine_device,
 | 
			
		||||
                    playready_device,
 | 
			
		||||
                },
 | 
			
		||||
                "*"
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data.type === "__GET_INJECTION_TYPE__") {
 | 
			
		||||
        chrome.storage.local.get("injection_type", (result) => {
 | 
			
		||||
            const injectionType = result.injection_type || "LICENSE";
 | 
			
		||||
 | 
			
		||||
            window.postMessage(
 | 
			
		||||
                {
 | 
			
		||||
                    type: "__INJECTION_TYPE__",
 | 
			
		||||
                    injectionType,
 | 
			
		||||
                },
 | 
			
		||||
                "*"
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data.type === "__GET_DRM_OVERRIDE__") {
 | 
			
		||||
        chrome.storage.local.get("drm_override", (result) => {
 | 
			
		||||
            const drmOverride = result.drm_override || "DISABLED";
 | 
			
		||||
 | 
			
		||||
            window.postMessage(
 | 
			
		||||
                {
 | 
			
		||||
                    type: "__DRM_OVERRIDE__",
 | 
			
		||||
                    drmOverride,
 | 
			
		||||
                },
 | 
			
		||||
                "*"
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Manifest header and URL
 | 
			
		||||
 | 
			
		||||
    const seenManifestUrls = new Set();
 | 
			
		||||
 | 
			
		||||
    if (event.data?.type === "__MANIFEST_URL__") {
 | 
			
		||||
        const url = event.data.data;
 | 
			
		||||
        if (seenManifestUrls.has(url)) return;
 | 
			
		||||
        seenManifestUrls.add(url);
 | 
			
		||||
        console.log("[CDRM-Extension] ✅ [content.js] Unique manifest URL:", url);
 | 
			
		||||
 | 
			
		||||
        chrome.runtime.sendMessage({
 | 
			
		||||
            type: "MANIFEST_URL",
 | 
			
		||||
            data: url,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.data?.type === "__MANIFEST_HEADERS__") {
 | 
			
		||||
        const { url, headers } = event.data;
 | 
			
		||||
        console.log("[CDRM-Extension] [content.js] Manifest headers:", url, headers);
 | 
			
		||||
 | 
			
		||||
        chrome.runtime.sendMessage({
 | 
			
		||||
            type: "MANIFEST_HEADERS",
 | 
			
		||||
            url,
 | 
			
		||||
            headers,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										930
									
								
								src/inject.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										930
									
								
								src/inject.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,930 @@
 | 
			
		||||
let widevineDeviceInfo = null;
 | 
			
		||||
let playreadyDeviceInfo = null;
 | 
			
		||||
let originalChallenge = null;
 | 
			
		||||
let serviceCertFound = false;
 | 
			
		||||
let drmType = "NONE";
 | 
			
		||||
let psshFound = false;
 | 
			
		||||
let foundWidevinePssh = null;
 | 
			
		||||
let foundPlayreadyPssh = null;
 | 
			
		||||
let drmDecided = null;
 | 
			
		||||
let drmOverride = "DISABLED";
 | 
			
		||||
let interceptType = "DISABLED";
 | 
			
		||||
let remoteCDM = null;
 | 
			
		||||
let generateRequestCalled = false;
 | 
			
		||||
let remoteListenerMounted = false;
 | 
			
		||||
let injectionSuccess = false;
 | 
			
		||||
let foundChallengeInBody = false;
 | 
			
		||||
let licenseResponseCounter = 0;
 | 
			
		||||
let keysRetrieved = false;
 | 
			
		||||
 | 
			
		||||
const DRM_SIGNATURES = {
 | 
			
		||||
    WIDEVINE: "CAES",
 | 
			
		||||
    PLAYREADY: "PD94",
 | 
			
		||||
    SERVICE_CERT: "CAUS",
 | 
			
		||||
    WIDEVINE_INIT: "CAQ=",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const EXTENSION_PREFIX = "[CDRM EXTENSION]";
 | 
			
		||||
const PREFIX_COLOR = "black";
 | 
			
		||||
const PREFIX_BACKGROUND_COLOR = "yellow";
 | 
			
		||||
 | 
			
		||||
const logWithPrefix = (...args) => {
 | 
			
		||||
    const style = `color: ${PREFIX_COLOR}; background: ${PREFIX_BACKGROUND_COLOR}; font-weight: bold; padding: 2px 4px; border-radius: 2px;`;
 | 
			
		||||
    if (typeof args[0] === "string") {
 | 
			
		||||
        // If the first arg is a string, prepend the prefix
 | 
			
		||||
        console.log(`%c${EXTENSION_PREFIX}%c ${args[0]}`, style, "", ...args.slice(1));
 | 
			
		||||
    } else {
 | 
			
		||||
        // If not, just log the prefix and the rest
 | 
			
		||||
        console.log(`%c${EXTENSION_PREFIX}`, style, ...args);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function resetDRMState() {
 | 
			
		||||
    logWithPrefix("Resetting DRM state for new manifest...");
 | 
			
		||||
 | 
			
		||||
    // Reset DRM detection state
 | 
			
		||||
    originalChallenge = null;
 | 
			
		||||
    serviceCertFound = false;
 | 
			
		||||
    drmType = "NONE";
 | 
			
		||||
    psshFound = false;
 | 
			
		||||
    foundWidevinePssh = null;
 | 
			
		||||
    foundPlayreadyPssh = null;
 | 
			
		||||
    drmDecided = null;
 | 
			
		||||
 | 
			
		||||
    // Reset CDM and session state
 | 
			
		||||
    if (remoteCDM) {
 | 
			
		||||
        try {
 | 
			
		||||
            // Try to close the existing session if it exists
 | 
			
		||||
            if (remoteCDM.session_id) {
 | 
			
		||||
                remoteCDM.closeSession();
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            // Ignore errors when closing session
 | 
			
		||||
            logWithPrefix("Error closing previous CDM session:", e.message);
 | 
			
		||||
        }
 | 
			
		||||
        remoteCDM = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Reset interceptor state
 | 
			
		||||
    generateRequestCalled = false;
 | 
			
		||||
    remoteListenerMounted = false;
 | 
			
		||||
    injectionSuccess = false;
 | 
			
		||||
    foundChallengeInBody = false;
 | 
			
		||||
    licenseResponseCounter = 0;
 | 
			
		||||
    keysRetrieved = false;
 | 
			
		||||
 | 
			
		||||
    // Post reset messages to clear UI state
 | 
			
		||||
    window.postMessage({ type: "__DRM_TYPE__", data: "" }, "*");
 | 
			
		||||
    window.postMessage({ type: "__PSSH_DATA__", data: "" }, "*");
 | 
			
		||||
    window.postMessage({ type: "__KEYS_DATA__", data: "" }, "*");
 | 
			
		||||
    window.postMessage({ type: "__LICENSE_URL__", data: "" }, "*");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.postMessage({ type: "__GET_DRM_OVERRIDE__" }, "*");
 | 
			
		||||
window.postMessage({ type: "__GET_INJECTION_TYPE__" }, "*");
 | 
			
		||||
window.postMessage({ type: "__GET_CDM_DEVICES__" }, "*");
 | 
			
		||||
 | 
			
		||||
function createMessageHandler(handlers) {
 | 
			
		||||
    window.addEventListener("message", function (event) {
 | 
			
		||||
        if (event.source !== window) return;
 | 
			
		||||
 | 
			
		||||
        const handler = handlers[event.data.type];
 | 
			
		||||
        if (handler) {
 | 
			
		||||
            handler(event.data);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
createMessageHandler({
 | 
			
		||||
    __DRM_OVERRIDE__: (data) => {
 | 
			
		||||
        drmOverride = data.drmOverride || "DISABLED";
 | 
			
		||||
        logWithPrefix("DRM Override set to:", drmOverride);
 | 
			
		||||
    },
 | 
			
		||||
    __INJECTION_TYPE__: (data) => {
 | 
			
		||||
        interceptType = data.injectionType || "DISABLED";
 | 
			
		||||
        logWithPrefix("Injection type set to:", interceptType);
 | 
			
		||||
    },
 | 
			
		||||
    __CDM_DEVICES__: (data) => {
 | 
			
		||||
        const { widevine_device, playready_device } = data;
 | 
			
		||||
        logWithPrefix("Received device info:", widevine_device, playready_device);
 | 
			
		||||
        widevineDeviceInfo = widevine_device;
 | 
			
		||||
        playreadyDeviceInfo = playready_device;
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function safeHeaderShellEscape(str) {
 | 
			
		||||
    return str
 | 
			
		||||
        .replace(/\\/g, "\\\\")
 | 
			
		||||
        .replace(/"/g, '\\"')
 | 
			
		||||
        .replace(/\$/g, "\\$") // escape shell expansion
 | 
			
		||||
        .replace(/`/g, "\\`")
 | 
			
		||||
        .replace(/\n/g, ""); // strip newlines
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function headersToFlags(headersObj) {
 | 
			
		||||
    return Object.entries(headersObj)
 | 
			
		||||
        .map(
 | 
			
		||||
            ([key, val]) =>
 | 
			
		||||
                '--add-headers "' +
 | 
			
		||||
                safeHeaderShellEscape(key) +
 | 
			
		||||
                ": " +
 | 
			
		||||
                safeHeaderShellEscape(val) +
 | 
			
		||||
                '"'
 | 
			
		||||
        )
 | 
			
		||||
        .join(" ");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleManifestDetection(url, headersObj, contentType, source) {
 | 
			
		||||
    // Reset DRM state when new manifest is detected
 | 
			
		||||
    resetDRMState();
 | 
			
		||||
 | 
			
		||||
    window.postMessage({ type: "__MANIFEST_URL__", data: url }, "*");
 | 
			
		||||
    logWithPrefix(`[Manifest][${source}]`, url, contentType);
 | 
			
		||||
 | 
			
		||||
    const headerFlags = headersToFlags(headersObj);
 | 
			
		||||
 | 
			
		||||
    window.postMessage(
 | 
			
		||||
        {
 | 
			
		||||
            type: "__MANIFEST_HEADERS__",
 | 
			
		||||
            url,
 | 
			
		||||
            headers: headerFlags,
 | 
			
		||||
        },
 | 
			
		||||
        "*"
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Intercep network to find manifest
 | 
			
		||||
function injectManifestInterceptor() {
 | 
			
		||||
    // Execute the interceptor code directly instead of injecting a script
 | 
			
		||||
    (function () {
 | 
			
		||||
        function isProbablyManifest(text = "", contentType = "") {
 | 
			
		||||
            const lowerCT = contentType?.toLowerCase() ?? "";
 | 
			
		||||
            const sample = text.slice(0, 2000);
 | 
			
		||||
 | 
			
		||||
            const isHLSMime = lowerCT.includes("mpegurl");
 | 
			
		||||
            const isDASHMime = lowerCT.includes("dash+xml");
 | 
			
		||||
            const isSmoothMime = lowerCT.includes("sstr+xml");
 | 
			
		||||
 | 
			
		||||
            const isHLSKeyword = sample.includes("#EXTM3U") || sample.includes("#EXT-X-STREAM-INF");
 | 
			
		||||
            const isDASHKeyword = sample.includes("<MPD") || sample.includes("<AdaptationSet");
 | 
			
		||||
            const isSmoothKeyword = sample.includes("<SmoothStreamingMedia");
 | 
			
		||||
            const isJsonManifest = sample.includes('"playlist"') && sample.includes('"segments"');
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                isHLSMime ||
 | 
			
		||||
                isDASHMime ||
 | 
			
		||||
                isSmoothMime ||
 | 
			
		||||
                isHLSKeyword ||
 | 
			
		||||
                isDASHKeyword ||
 | 
			
		||||
                isSmoothKeyword ||
 | 
			
		||||
                isJsonManifest
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const originalFetch = window.fetch;
 | 
			
		||||
        window.fetch = async function (input, init) {
 | 
			
		||||
            const response = await originalFetch.apply(this, arguments);
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const clone = response.clone();
 | 
			
		||||
                const contentType = clone.headers.get("content-type") || "";
 | 
			
		||||
                const text = await clone.text();
 | 
			
		||||
 | 
			
		||||
                const url = typeof input === "string" ? input : input.url;
 | 
			
		||||
 | 
			
		||||
                if (isProbablyManifest(text, contentType)) {
 | 
			
		||||
                    const headersObj = {};
 | 
			
		||||
                    clone.headers.forEach((value, key) => {
 | 
			
		||||
                        headersObj[key] = value;
 | 
			
		||||
                    });
 | 
			
		||||
                    handleManifestDetection(url, headersObj, contentType, "fetch");
 | 
			
		||||
                }
 | 
			
		||||
            } catch (e) {}
 | 
			
		||||
 | 
			
		||||
            return response;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const originalXHROpen = XMLHttpRequest.prototype.open;
 | 
			
		||||
        const originalXHRSend = XMLHttpRequest.prototype.send;
 | 
			
		||||
 | 
			
		||||
        XMLHttpRequest.prototype.open = function (method, url) {
 | 
			
		||||
            this.__url = url;
 | 
			
		||||
            return originalXHROpen.apply(this, arguments);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        XMLHttpRequest.prototype.send = function (body) {
 | 
			
		||||
            this.addEventListener("load", function () {
 | 
			
		||||
                try {
 | 
			
		||||
                    const contentType = this.getResponseHeader("content-type") || "";
 | 
			
		||||
                    const text = this.responseText;
 | 
			
		||||
 | 
			
		||||
                    if (isProbablyManifest(text, contentType)) {
 | 
			
		||||
                        const xhrHeaders = {};
 | 
			
		||||
                        const rawHeaders = this.getAllResponseHeaders().trim().split(/\r?\n/);
 | 
			
		||||
                        rawHeaders.forEach((line) => {
 | 
			
		||||
                            const parts = line.split(": ");
 | 
			
		||||
                            if (parts.length === 2) {
 | 
			
		||||
                                xhrHeaders[parts[0]] = parts[1];
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
                        handleManifestDetection(this.__url, xhrHeaders, contentType, "xhr");
 | 
			
		||||
                    }
 | 
			
		||||
                } catch (e) {}
 | 
			
		||||
            });
 | 
			
		||||
            return originalXHRSend.apply(this, arguments);
 | 
			
		||||
        };
 | 
			
		||||
    })();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
injectManifestInterceptor();
 | 
			
		||||
 | 
			
		||||
class RemoteCDMBase {
 | 
			
		||||
    constructor({ host, secret, device_name, security_level }) {
 | 
			
		||||
        this.host = host;
 | 
			
		||||
        this.secret = secret;
 | 
			
		||||
        this.device_name = device_name;
 | 
			
		||||
        this.security_level = security_level;
 | 
			
		||||
        this.session_id = null;
 | 
			
		||||
        this.challenge = null;
 | 
			
		||||
        this.keys = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    openSession(path) {
 | 
			
		||||
        const url = `${this.host}${path}/open`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open("GET", url, false);
 | 
			
		||||
        xhr.setRequestHeader("Content-Type", "application/json");
 | 
			
		||||
        xhr.send();
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.session_id) {
 | 
			
		||||
            this.session_id = jsonData.data.session_id;
 | 
			
		||||
            logWithPrefix("Session opened:", this.session_id);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to open session:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to open session");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChallenge(path, body) {
 | 
			
		||||
        const url = `${this.host}${path}/get_license_challenge`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open("POST", url, false);
 | 
			
		||||
        xhr.setRequestHeader("Content-Type", "application/json");
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.challenge) {
 | 
			
		||||
            this.challenge = btoa(jsonData.data.challenge);
 | 
			
		||||
            logWithPrefix("Challenge received:", this.challenge);
 | 
			
		||||
        } else if (jsonData.data?.challenge_b64) {
 | 
			
		||||
            this.challenge = jsonData.data.challenge_b64;
 | 
			
		||||
            logWithPrefix("Challenge received:", this.challenge);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to get challenge:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to get challenge");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    parseLicense(path, body) {
 | 
			
		||||
        const url = `${this.host}${path}/parse_license`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open("POST", url, false);
 | 
			
		||||
        xhr.setRequestHeader("Content-Type", "application/json");
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (
 | 
			
		||||
            xhr.status === 200 ||
 | 
			
		||||
            jsonData.status === "Success" ||
 | 
			
		||||
            jsonData.status === 200 ||
 | 
			
		||||
            jsonData.message?.includes("parsed and loaded")
 | 
			
		||||
        ) {
 | 
			
		||||
            logWithPrefix("License response parsed successfully");
 | 
			
		||||
            return true;
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to parse license response:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to parse license response");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getKeys(path, body, extraPath = "") {
 | 
			
		||||
        const url = `${this.host}${path}/get_keys${extraPath}`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open("POST", url, false);
 | 
			
		||||
        xhr.setRequestHeader("Content-Type", "application/json");
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.keys) {
 | 
			
		||||
            this.keys = jsonData.data.keys;
 | 
			
		||||
            logWithPrefix("Keys received:", this.keys);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to get keys:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to get keys");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    closeSession(path) {
 | 
			
		||||
        const url = `${this.host}${path}/close/${this.session_id}`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open("GET", url, false);
 | 
			
		||||
        xhr.setRequestHeader("Content-Type", "application/json");
 | 
			
		||||
        xhr.send();
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData) {
 | 
			
		||||
            logWithPrefix("Session closed successfully");
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to close session:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to close session");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PlayReady Remote CDM Class
 | 
			
		||||
class remotePlayReadyCDM extends RemoteCDMBase {
 | 
			
		||||
    constructor(security_level, host, secret, device_name) {
 | 
			
		||||
        super({ host, secret, device_name, security_level });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    openSession() {
 | 
			
		||||
        super.openSession(`/remotecdm/playready/${this.device_name}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChallenge(init_data) {
 | 
			
		||||
        super.getChallenge(`/remotecdm/playready/${this.device_name}`, {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            init_data: init_data,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    parseLicense(license_message) {
 | 
			
		||||
        return super.parseLicense(`/remotecdm/playready/${this.device_name}`, {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            license_message: license_message,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getKeys() {
 | 
			
		||||
        super.getKeys(`/remotecdm/playready/${this.device_name}`, {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    closeSession() {
 | 
			
		||||
        super.closeSession(`/remotecdm/playready/${this.device_name}`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Widevine Remote CDM Class
 | 
			
		||||
class remoteWidevineCDM extends RemoteCDMBase {
 | 
			
		||||
    constructor(device_type, system_id, security_level, host, secret, device_name) {
 | 
			
		||||
        super({ host, secret, device_name, security_level });
 | 
			
		||||
        this.device_type = device_type;
 | 
			
		||||
        this.system_id = system_id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    openSession() {
 | 
			
		||||
        super.openSession(`/remotecdm/widevine/${this.device_name}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setServiceCertificate(certificate) {
 | 
			
		||||
        const url = `${this.host}/remotecdm/widevine/${this.device_name}/set_service_certificate`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open("POST", url, false);
 | 
			
		||||
        xhr.setRequestHeader("Content-Type", "application/json");
 | 
			
		||||
        const body = {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            certificate: certificate ?? null,
 | 
			
		||||
        };
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (xhr.status === 200 || jsonData.status === "Success" || jsonData.status === 200) {
 | 
			
		||||
            logWithPrefix("Service certificate set successfully");
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to set service certificate:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to set service certificate");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChallenge(init_data, license_type = "STREAMING") {
 | 
			
		||||
        const url = `${this.host}/remotecdm/widevine/${this.device_name}/get_license_challenge/${license_type}`;
 | 
			
		||||
        const xhr = new XMLHttpRequest();
 | 
			
		||||
        xhr.open("POST", url, false);
 | 
			
		||||
        xhr.setRequestHeader("Content-Type", "application/json");
 | 
			
		||||
        const body = {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            init_data: init_data,
 | 
			
		||||
            privacy_mode: serviceCertFound,
 | 
			
		||||
        };
 | 
			
		||||
        xhr.send(JSON.stringify(body));
 | 
			
		||||
        const jsonData = JSON.parse(xhr.responseText);
 | 
			
		||||
        if (jsonData.data?.challenge_b64) {
 | 
			
		||||
            this.challenge = jsonData.data.challenge_b64;
 | 
			
		||||
            logWithPrefix("Widevine challenge received:", this.challenge);
 | 
			
		||||
        } else {
 | 
			
		||||
            console.error("Failed to get Widevine challenge:", jsonData.message);
 | 
			
		||||
            throw new Error("Failed to get Widevine challenge");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    parseLicense(license_message) {
 | 
			
		||||
        return super.parseLicense(`/remotecdm/widevine/${this.device_name}`, {
 | 
			
		||||
            session_id: this.session_id,
 | 
			
		||||
            license_message: license_message,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getKeys() {
 | 
			
		||||
        super.getKeys(
 | 
			
		||||
            `/remotecdm/widevine/${this.device_name}`,
 | 
			
		||||
            {
 | 
			
		||||
                session_id: this.session_id,
 | 
			
		||||
            },
 | 
			
		||||
            "/ALL"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    closeSession() {
 | 
			
		||||
        super.closeSession(`/remotecdm/widevine/${this.device_name}`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Utility functions
 | 
			
		||||
function hexStrToU8(hexString) {
 | 
			
		||||
    return Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function u8ToHexStr(bytes) {
 | 
			
		||||
    return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function b64ToHexStr(b64) {
 | 
			
		||||
    return [...atob(b64)].map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join``;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function jsonContainsValue(obj, prefix = DRM_SIGNATURES.WIDEVINE) {
 | 
			
		||||
    if (typeof obj === "string") return obj.startsWith(prefix);
 | 
			
		||||
    if (Array.isArray(obj)) return obj.some((val) => jsonContainsValue(val, prefix));
 | 
			
		||||
    if (typeof obj === "object" && obj !== null) {
 | 
			
		||||
        return Object.values(obj).some((val) => jsonContainsValue(val, prefix));
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function jsonReplaceValue(obj, newValue) {
 | 
			
		||||
    if (typeof obj === "string") {
 | 
			
		||||
        return obj.startsWith(DRM_SIGNATURES.WIDEVINE) || obj.startsWith(DRM_SIGNATURES.PLAYREADY)
 | 
			
		||||
            ? newValue
 | 
			
		||||
            : obj;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (Array.isArray(obj)) {
 | 
			
		||||
        return obj.map((item) => jsonReplaceValue(item, newValue));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (typeof obj === "object" && obj !== null) {
 | 
			
		||||
        const newObj = {};
 | 
			
		||||
        for (const key in obj) {
 | 
			
		||||
            if (Object.hasOwn(obj, key)) {
 | 
			
		||||
                newObj[key] = jsonReplaceValue(obj[key], newValue);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return newObj;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return obj;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isJson(str) {
 | 
			
		||||
    try {
 | 
			
		||||
        JSON.parse(str);
 | 
			
		||||
        return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getWidevinePssh(buffer) {
 | 
			
		||||
    const hex = u8ToHexStr(new Uint8Array(buffer));
 | 
			
		||||
    const match = hex.match(/000000(..)?70737368.*/);
 | 
			
		||||
    if (!match) return null;
 | 
			
		||||
 | 
			
		||||
    const boxHex = match[0];
 | 
			
		||||
    const bytes = hexStrToU8(boxHex);
 | 
			
		||||
    return window.btoa(String.fromCharCode(...bytes));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getPlayReadyPssh(buffer) {
 | 
			
		||||
    const u8 = new Uint8Array(buffer);
 | 
			
		||||
    const systemId = "9a04f07998404286ab92e65be0885f95";
 | 
			
		||||
    const hex = u8ToHexStr(u8);
 | 
			
		||||
    const index = hex.indexOf(systemId);
 | 
			
		||||
    if (index === -1) return null;
 | 
			
		||||
    const psshBoxStart = hex.lastIndexOf("70737368", index);
 | 
			
		||||
    if (psshBoxStart === -1) return null;
 | 
			
		||||
    const lenStart = psshBoxStart - 8;
 | 
			
		||||
    const boxLen = parseInt(hex.substr(lenStart, 8), 16) * 2;
 | 
			
		||||
    const psshHex = hex.substr(lenStart, boxLen);
 | 
			
		||||
    const psshBytes = hexStrToU8(psshHex);
 | 
			
		||||
    return window.btoa(String.fromCharCode(...psshBytes));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getClearkey(response) {
 | 
			
		||||
    let obj = JSON.parse(new TextDecoder("utf-8").decode(response));
 | 
			
		||||
    return obj["keys"].map((o) => ({
 | 
			
		||||
        key_id: b64ToHexStr(o["kid"].replace(/-/g, "+").replace(/_/g, "/")),
 | 
			
		||||
        key: b64ToHexStr(o["k"].replace(/-/g, "+").replace(/_/g, "/")),
 | 
			
		||||
    }));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function base64ToUint8Array(base64) {
 | 
			
		||||
    const binaryStr = atob(base64);
 | 
			
		||||
    const len = binaryStr.length;
 | 
			
		||||
    const bytes = new Uint8Array(len);
 | 
			
		||||
    for (let i = 0; i < len; i++) {
 | 
			
		||||
        bytes[i] = binaryStr.charCodeAt(i);
 | 
			
		||||
    }
 | 
			
		||||
    return bytes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function arrayBufferToBase64(uint8array) {
 | 
			
		||||
    let binary = "";
 | 
			
		||||
    const len = uint8array.length;
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < len; i++) {
 | 
			
		||||
        binary += String.fromCharCode(uint8array[i]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return window.btoa(binary);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function bufferToBase64(buffer) {
 | 
			
		||||
    const uint8 = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
 | 
			
		||||
    return window.btoa(String.fromCharCode(...uint8));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DRM type detection
 | 
			
		||||
function isWidevine(base64str) {
 | 
			
		||||
    return base64str.startsWith(DRM_SIGNATURES.WIDEVINE);
 | 
			
		||||
}
 | 
			
		||||
function isPlayReady(base64str) {
 | 
			
		||||
    return base64str.startsWith(DRM_SIGNATURES.PLAYREADY);
 | 
			
		||||
}
 | 
			
		||||
function isServiceCertificate(base64str) {
 | 
			
		||||
    return base64str.startsWith(DRM_SIGNATURES.SERVICE_CERT);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function postDRMTypeAndPssh(type, pssh) {
 | 
			
		||||
    window.postMessage({ type: "__DRM_TYPE__", data: type }, "*");
 | 
			
		||||
    window.postMessage({ type: "__PSSH_DATA__", data: pssh }, "*");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createAndOpenRemoteCDM(type, deviceInfo, pssh) {
 | 
			
		||||
    let cdm;
 | 
			
		||||
    if (type === "Widevine") {
 | 
			
		||||
        const { device_type, system_id, security_level, host, secret, device_name } = deviceInfo;
 | 
			
		||||
        cdm = new remoteWidevineCDM(
 | 
			
		||||
            device_type,
 | 
			
		||||
            system_id,
 | 
			
		||||
            security_level,
 | 
			
		||||
            host,
 | 
			
		||||
            secret,
 | 
			
		||||
            device_name
 | 
			
		||||
        );
 | 
			
		||||
        cdm.openSession();
 | 
			
		||||
        cdm.getChallenge(pssh);
 | 
			
		||||
    } else if (type === "PlayReady") {
 | 
			
		||||
        const { security_level, host, secret, device_name } = deviceInfo;
 | 
			
		||||
        cdm = new remotePlayReadyCDM(security_level, host, secret, device_name);
 | 
			
		||||
        cdm.openSession();
 | 
			
		||||
        cdm.getChallenge(pssh);
 | 
			
		||||
    }
 | 
			
		||||
    return cdm;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ensureRemoteCDM(type, deviceInfo, pssh) {
 | 
			
		||||
    if (!remoteCDM) {
 | 
			
		||||
        remoteCDM = createAndOpenRemoteCDM(type, deviceInfo, pssh);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function detectAndStorePssh(initData) {
 | 
			
		||||
    const detections = [
 | 
			
		||||
        {
 | 
			
		||||
            type: "PlayReady",
 | 
			
		||||
            getter: getPlayReadyPssh,
 | 
			
		||||
            store: (pssh) => (foundPlayreadyPssh = pssh),
 | 
			
		||||
        },
 | 
			
		||||
        { type: "Widevine", getter: getWidevinePssh, store: (pssh) => (foundWidevinePssh = pssh) },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    detections.forEach(({ type, getter, store }) => {
 | 
			
		||||
        const pssh = getter(initData);
 | 
			
		||||
        if (pssh) {
 | 
			
		||||
            logWithPrefix(`[DRM Detected] ${type}`);
 | 
			
		||||
            store(pssh);
 | 
			
		||||
            logWithPrefix(`[${type} PSSH found] ${pssh}`);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Challenge generator interceptor
 | 
			
		||||
const originalGenerateRequest = MediaKeySession.prototype.generateRequest;
 | 
			
		||||
MediaKeySession.prototype.generateRequest = function (initDataType, initData) {
 | 
			
		||||
    const session = this;
 | 
			
		||||
    detectAndStorePssh(initData);
 | 
			
		||||
 | 
			
		||||
    // Challenge message interceptor
 | 
			
		||||
    if (!remoteListenerMounted) {
 | 
			
		||||
        remoteListenerMounted = true;
 | 
			
		||||
        session.addEventListener("message", function messageInterceptor(event) {
 | 
			
		||||
            event.stopImmediatePropagation();
 | 
			
		||||
            const base64challenge = bufferToBase64(event.message);
 | 
			
		||||
            if (
 | 
			
		||||
                base64challenge === DRM_SIGNATURES.WIDEVINE_INIT &&
 | 
			
		||||
                interceptType !== "DISABLED" &&
 | 
			
		||||
                !serviceCertFound
 | 
			
		||||
            ) {
 | 
			
		||||
                remoteCDM = createAndOpenRemoteCDM(
 | 
			
		||||
                    "Widevine",
 | 
			
		||||
                    widevineDeviceInfo,
 | 
			
		||||
                    foundWidevinePssh
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            if (
 | 
			
		||||
                !injectionSuccess &&
 | 
			
		||||
                base64challenge !== DRM_SIGNATURES.WIDEVINE_INIT &&
 | 
			
		||||
                interceptType !== "DISABLED"
 | 
			
		||||
            ) {
 | 
			
		||||
                if (interceptType === "EME") {
 | 
			
		||||
                    injectionSuccess = true;
 | 
			
		||||
                }
 | 
			
		||||
                if (!originalChallenge) {
 | 
			
		||||
                    originalChallenge = base64challenge;
 | 
			
		||||
                }
 | 
			
		||||
                if (originalChallenge.startsWith(DRM_SIGNATURES.WIDEVINE)) {
 | 
			
		||||
                    postDRMTypeAndPssh("Widevine", foundWidevinePssh);
 | 
			
		||||
                    if (interceptType === "EME") {
 | 
			
		||||
                        ensureRemoteCDM("Widevine", widevineDeviceInfo, foundWidevinePssh);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (!originalChallenge.startsWith(DRM_SIGNATURES.WIDEVINE)) {
 | 
			
		||||
                    const buffer = event.message;
 | 
			
		||||
                    const decoder = new TextDecoder("utf-16");
 | 
			
		||||
                    const decodedText = decoder.decode(buffer);
 | 
			
		||||
                    const match = decodedText.match(
 | 
			
		||||
                        /<Challenge encoding="base64encoded">([^<]+)<\/Challenge>/
 | 
			
		||||
                    );
 | 
			
		||||
                    if (match) {
 | 
			
		||||
                        postDRMTypeAndPssh("PlayReady", foundPlayreadyPssh);
 | 
			
		||||
                        originalChallenge = match[1];
 | 
			
		||||
                        if (interceptType === "EME") {
 | 
			
		||||
                            ensureRemoteCDM("PlayReady", playreadyDeviceInfo, foundPlayreadyPssh);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (interceptType === "EME" && remoteCDM) {
 | 
			
		||||
                    const uint8challenge = base64ToUint8Array(remoteCDM.challenge);
 | 
			
		||||
                    const challengeBuffer = uint8challenge.buffer;
 | 
			
		||||
                    const syntheticEvent = new MessageEvent("message", {
 | 
			
		||||
                        data: event.data,
 | 
			
		||||
                        origin: event.origin,
 | 
			
		||||
                        lastEventId: event.lastEventId,
 | 
			
		||||
                        source: event.source,
 | 
			
		||||
                        ports: event.ports,
 | 
			
		||||
                    });
 | 
			
		||||
                    Object.defineProperty(syntheticEvent, "message", {
 | 
			
		||||
                        get: () => challengeBuffer,
 | 
			
		||||
                    });
 | 
			
		||||
                    logWithPrefix("Intercepted EME Challenge and injected custom one.");
 | 
			
		||||
                    session.dispatchEvent(syntheticEvent);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        logWithPrefix("Message interceptor mounted.");
 | 
			
		||||
    }
 | 
			
		||||
    return originalGenerateRequest.call(session, initDataType, initData);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Message update interceptors
 | 
			
		||||
const originalUpdate = MediaKeySession.prototype.update;
 | 
			
		||||
MediaKeySession.prototype.update = function (response) {
 | 
			
		||||
    const base64Response = bufferToBase64(response);
 | 
			
		||||
    if (
 | 
			
		||||
        base64Response.startsWith(DRM_SIGNATURES.SERVICE_CERT) &&
 | 
			
		||||
        foundWidevinePssh &&
 | 
			
		||||
        remoteCDM &&
 | 
			
		||||
        !serviceCertFound
 | 
			
		||||
    ) {
 | 
			
		||||
        remoteCDM.setServiceCertificate(base64Response);
 | 
			
		||||
        if (interceptType === "EME" && !remoteCDM.challenge) {
 | 
			
		||||
            remoteCDM.getChallenge(foundWidevinePssh);
 | 
			
		||||
        }
 | 
			
		||||
        window.postMessage({ type: "__DRM_TYPE__", data: "Widevine" }, "*");
 | 
			
		||||
        window.postMessage({ type: "__PSSH_DATA__", data: foundWidevinePssh }, "*");
 | 
			
		||||
        serviceCertFound = true;
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
        !base64Response.startsWith(DRM_SIGNATURES.SERVICE_CERT) &&
 | 
			
		||||
        (foundWidevinePssh || foundPlayreadyPssh) &&
 | 
			
		||||
        !keysRetrieved
 | 
			
		||||
    ) {
 | 
			
		||||
        if (licenseResponseCounter === 1 || foundChallengeInBody) {
 | 
			
		||||
            remoteCDM.parseLicense(base64Response);
 | 
			
		||||
            remoteCDM.getKeys();
 | 
			
		||||
            remoteCDM.closeSession();
 | 
			
		||||
            keysRetrieved = true;
 | 
			
		||||
            window.postMessage({ type: "__KEYS_DATA__", data: remoteCDM.keys }, "*");
 | 
			
		||||
        }
 | 
			
		||||
        licenseResponseCounter++;
 | 
			
		||||
    }
 | 
			
		||||
    const updatePromise = originalUpdate.call(this, response);
 | 
			
		||||
    if (!foundPlayreadyPssh && !foundWidevinePssh) {
 | 
			
		||||
        updatePromise
 | 
			
		||||
            .then(() => {
 | 
			
		||||
                let clearKeys = getClearkey(response);
 | 
			
		||||
                if (clearKeys && clearKeys.length > 0) {
 | 
			
		||||
                    logWithPrefix("[CLEARKEY] ", clearKeys);
 | 
			
		||||
                    const drmType = {
 | 
			
		||||
                        type: "__DRM_TYPE__",
 | 
			
		||||
                        data: "ClearKey",
 | 
			
		||||
                    };
 | 
			
		||||
                    window.postMessage(drmType, "*");
 | 
			
		||||
                    const keysData = {
 | 
			
		||||
                        type: "__KEYS_DATA__",
 | 
			
		||||
                        data: clearKeys,
 | 
			
		||||
                    };
 | 
			
		||||
                    window.postMessage(keysData, "*");
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .catch((e) => {
 | 
			
		||||
                logWithPrefix("[CLEARKEY] Not found");
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return updatePromise;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Helpers
 | 
			
		||||
function detectDRMChallenge(body) {
 | 
			
		||||
    // Handles ArrayBuffer, Uint8Array, string, and JSON string
 | 
			
		||||
    // Returns: { type: "Widevine"|"PlayReady"|null, base64: string|null, bodyType: "buffer"|"string"|"json"|null }
 | 
			
		||||
    if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
 | 
			
		||||
        const buffer = body instanceof Uint8Array ? body : new Uint8Array(body);
 | 
			
		||||
        const base64Body = window.btoa(String.fromCharCode(...buffer));
 | 
			
		||||
        if (base64Body.startsWith(DRM_SIGNATURES.WIDEVINE)) {
 | 
			
		||||
            return { type: "Widevine", base64: base64Body, bodyType: "buffer" };
 | 
			
		||||
        }
 | 
			
		||||
        if (base64Body.startsWith(DRM_SIGNATURES.PLAYREADY)) {
 | 
			
		||||
            return { type: "PlayReady", base64: base64Body, bodyType: "buffer" };
 | 
			
		||||
        }
 | 
			
		||||
    } else if (typeof body === "string" && !isJson(body)) {
 | 
			
		||||
        const base64EncodedBody = btoa(body);
 | 
			
		||||
        if (base64EncodedBody.startsWith(DRM_SIGNATURES.WIDEVINE)) {
 | 
			
		||||
            return { type: "Widevine", base64: base64EncodedBody, bodyType: "string" };
 | 
			
		||||
        }
 | 
			
		||||
        if (base64EncodedBody.startsWith(DRM_SIGNATURES.PLAYREADY)) {
 | 
			
		||||
            return { type: "PlayReady", base64: base64EncodedBody, bodyType: "string" };
 | 
			
		||||
        }
 | 
			
		||||
    } else if (typeof body === "string" && isJson(body)) {
 | 
			
		||||
        const jsonBody = JSON.parse(body);
 | 
			
		||||
        if (jsonContainsValue(jsonBody, DRM_SIGNATURES.WIDEVINE)) {
 | 
			
		||||
            return { type: "Widevine", base64: null, bodyType: "json" };
 | 
			
		||||
        }
 | 
			
		||||
        if (jsonContainsValue(jsonBody, DRM_SIGNATURES.PLAYREADY)) {
 | 
			
		||||
            return { type: "PlayReady", base64: null, bodyType: "json" };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return { type: null, base64: null, bodyType: null };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleLicenseMode({
 | 
			
		||||
    drmInfo,
 | 
			
		||||
    body,
 | 
			
		||||
    setBody, // function to set the new body (for fetch: (b) => config.body = b, for XHR: (b) => originalSend.call(this, b))
 | 
			
		||||
    urlOrResource,
 | 
			
		||||
    getWidevinePssh,
 | 
			
		||||
    getPlayreadyPssh,
 | 
			
		||||
    widevineDeviceInfo,
 | 
			
		||||
    playreadyDeviceInfo,
 | 
			
		||||
}) {
 | 
			
		||||
    foundChallengeInBody = true;
 | 
			
		||||
    window.postMessage({ type: "__LICENSE_URL__", data: urlOrResource }, "*");
 | 
			
		||||
 | 
			
		||||
    // Create remoteCDM if needed
 | 
			
		||||
    if (!remoteCDM) {
 | 
			
		||||
        if (drmInfo.type === "Widevine") {
 | 
			
		||||
            remoteCDM = createAndOpenRemoteCDM("Widevine", widevineDeviceInfo, getWidevinePssh());
 | 
			
		||||
        }
 | 
			
		||||
        if (drmInfo.type === "PlayReady") {
 | 
			
		||||
            remoteCDM = createAndOpenRemoteCDM(
 | 
			
		||||
                "PlayReady",
 | 
			
		||||
                playreadyDeviceInfo,
 | 
			
		||||
                getPlayreadyPssh()
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (remoteCDM && remoteCDM.challenge === null) {
 | 
			
		||||
        remoteCDM.getChallenge(getWidevinePssh());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Inject the new challenge into the request body
 | 
			
		||||
    if (drmInfo.bodyType === "json") {
 | 
			
		||||
        const jsonBody = JSON.parse(body);
 | 
			
		||||
        const injectedBody = jsonReplaceValue(jsonBody, remoteCDM.challenge);
 | 
			
		||||
        setBody(JSON.stringify(injectedBody));
 | 
			
		||||
    } else if (drmInfo.bodyType === "buffer") {
 | 
			
		||||
        setBody(base64ToUint8Array(remoteCDM.challenge));
 | 
			
		||||
    } else {
 | 
			
		||||
        setBody(atob(remoteCDM.challenge));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleDRMInterception(drmInfo, body, url, setBodyCallback, continueRequestCallback) {
 | 
			
		||||
    // EME mode: block the request if a DRM challenge is detected
 | 
			
		||||
    if (
 | 
			
		||||
        drmInfo.type &&
 | 
			
		||||
        (!remoteCDM || remoteCDM.challenge === null || drmInfo.base64 !== remoteCDM.challenge) &&
 | 
			
		||||
        interceptType === "EME"
 | 
			
		||||
    ) {
 | 
			
		||||
        foundChallengeInBody = true;
 | 
			
		||||
        window.postMessage({ type: "__LICENSE_URL__", data: url }, "*");
 | 
			
		||||
        // Block the request
 | 
			
		||||
        return { shouldBlock: true };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // LICENSE mode: replace the challenge in the request
 | 
			
		||||
    if (drmInfo.type && interceptType === "LICENSE" && !foundChallengeInBody) {
 | 
			
		||||
        handleLicenseMode({
 | 
			
		||||
            drmInfo,
 | 
			
		||||
            body,
 | 
			
		||||
            setBody: setBodyCallback,
 | 
			
		||||
            urlOrResource: url,
 | 
			
		||||
            getWidevinePssh: () => foundWidevinePssh,
 | 
			
		||||
            getPlayreadyPssh: () => foundPlayreadyPssh,
 | 
			
		||||
            widevineDeviceInfo,
 | 
			
		||||
            playreadyDeviceInfo,
 | 
			
		||||
        });
 | 
			
		||||
        return { shouldIntercept: true, result: continueRequestCallback() };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { shouldContinue: true };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// fetch POST interceptor
 | 
			
		||||
(function () {
 | 
			
		||||
    const originalFetch = window.fetch;
 | 
			
		||||
 | 
			
		||||
    window.fetch = async function (resource, config = {}) {
 | 
			
		||||
        const method = (config.method || "GET").toUpperCase();
 | 
			
		||||
 | 
			
		||||
        if (method === "POST" && config.body) {
 | 
			
		||||
            logWithPrefix("[FETCH] Intercepting POST request to:", resource);
 | 
			
		||||
            const drmInfo = detectDRMChallenge(config.body);
 | 
			
		||||
 | 
			
		||||
            const result = handleDRMInterception(
 | 
			
		||||
                drmInfo,
 | 
			
		||||
                config.body,
 | 
			
		||||
                resource,
 | 
			
		||||
                (b) => {
 | 
			
		||||
                    config.body = b;
 | 
			
		||||
                },
 | 
			
		||||
                () => originalFetch(resource, config)
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (result.shouldBlock) return;
 | 
			
		||||
            if (result.shouldIntercept) return result.result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return originalFetch(resource, config);
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
// XHR POST interceptor
 | 
			
		||||
(function () {
 | 
			
		||||
    const originalOpen = XMLHttpRequest.prototype.open;
 | 
			
		||||
    const originalSend = XMLHttpRequest.prototype.send;
 | 
			
		||||
 | 
			
		||||
    XMLHttpRequest.prototype.open = function (method, url, async, user, password) {
 | 
			
		||||
        this._method = method;
 | 
			
		||||
        this._url = url;
 | 
			
		||||
        return originalOpen.apply(this, arguments);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    XMLHttpRequest.prototype.send = function (body) {
 | 
			
		||||
        if (this._method && this._method.toUpperCase() === "POST" && body) {
 | 
			
		||||
            logWithPrefix("[XHR] Intercepting POST request to:", this._url);
 | 
			
		||||
            const drmInfo = detectDRMChallenge(body);
 | 
			
		||||
 | 
			
		||||
            const result = handleDRMInterception(
 | 
			
		||||
                drmInfo,
 | 
			
		||||
                body,
 | 
			
		||||
                this._url,
 | 
			
		||||
                (b) => originalSend.call(this, b),
 | 
			
		||||
                () => {} // XHR doesn't need continuation callback
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (result.shouldBlock) return;
 | 
			
		||||
            if (result.shouldIntercept) return result.result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return originalSend.apply(this, arguments);
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
							
								
								
									
										37
									
								
								src/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/manifest.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
{
 | 
			
		||||
    "manifest_version": 3,
 | 
			
		||||
    "name": "CDRM Extension",
 | 
			
		||||
    "version": "3.0",
 | 
			
		||||
    "description": "Decrypt DRM protected content",
 | 
			
		||||
    "permissions": ["storage", "activeTab", "contextMenus"],
 | 
			
		||||
    "host_permissions": ["<all_urls>"],
 | 
			
		||||
    "background": {
 | 
			
		||||
        "service_worker": "background.js"
 | 
			
		||||
    },
 | 
			
		||||
    "content_scripts": [
 | 
			
		||||
        {
 | 
			
		||||
            "matches": ["<all_urls>"],
 | 
			
		||||
            "js": ["content.js"],
 | 
			
		||||
            "run_at": "document_start",
 | 
			
		||||
            "all_frames": true
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "web_accessible_resources": [
 | 
			
		||||
        {
 | 
			
		||||
            "resources": ["inject.js"],
 | 
			
		||||
            "matches": ["<all_urls>"]
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "action": {
 | 
			
		||||
        "default_icon": {
 | 
			
		||||
            "16": "icons/icon16.png",
 | 
			
		||||
            "32": "icons/icon32.png",
 | 
			
		||||
            "128": "icons/icon128.png"
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "icons": {
 | 
			
		||||
        "16": "icons/icon16.png",
 | 
			
		||||
        "32": "icons/icon32.png",
 | 
			
		||||
        "128": "icons/icon128.png"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								syncVersion.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								syncVersion.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
import fs from "fs/promises";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import { fileURLToPath } from "url";
 | 
			
		||||
 | 
			
		||||
const __filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const __dirname = path.dirname(__filename);
 | 
			
		||||
 | 
			
		||||
const updateVersionWithRegex = async (filePath, newVersion) => {
 | 
			
		||||
    try {
 | 
			
		||||
        const content = await fs.readFile(filePath, "utf-8");
 | 
			
		||||
 | 
			
		||||
        // Regex to match "version": "any.version.number"
 | 
			
		||||
        const versionRegex = /("version"\s*:\s*")([^"]+)(")/;
 | 
			
		||||
 | 
			
		||||
        if (!versionRegex.test(content)) {
 | 
			
		||||
            console.warn(`⚠️ No version field found in ${filePath}`);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const updatedContent = content.replace(versionRegex, `$1${newVersion}$3`);
 | 
			
		||||
 | 
			
		||||
        if (content !== updatedContent) {
 | 
			
		||||
            await fs.writeFile(filePath, updatedContent);
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
        console.error(`❌ Failed to update ${filePath}: ${err.message}`);
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const syncVersion = async () => {
 | 
			
		||||
    const rootPkgPath = path.join(__dirname, "package.json");
 | 
			
		||||
    const frontendPkgPath = path.join(__dirname, "frontend", "package.json");
 | 
			
		||||
    const manifestPath = path.join(__dirname, "src", "manifest.json");
 | 
			
		||||
 | 
			
		||||
    // Read root package.json version
 | 
			
		||||
    const rootPkgRaw = await fs.readFile(rootPkgPath, "utf-8");
 | 
			
		||||
    const rootPkg = JSON.parse(rootPkgRaw);
 | 
			
		||||
    const version = rootPkg.version;
 | 
			
		||||
 | 
			
		||||
    if (!version) {
 | 
			
		||||
        console.warn("⚠️ No version field found in root package.json, skipping sync.");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Update frontend/package.json using regex
 | 
			
		||||
    const frontendUpdated = await updateVersionWithRegex(frontendPkgPath, version);
 | 
			
		||||
    if (frontendUpdated) {
 | 
			
		||||
        console.log(`🔄 Updated frontend/package.json version to ${version}`);
 | 
			
		||||
    } else {
 | 
			
		||||
        console.log("ℹ️ frontend/package.json not found or no changes needed.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Update src/manifest.json using regex
 | 
			
		||||
    const manifestUpdated = await updateVersionWithRegex(manifestPath, version);
 | 
			
		||||
    if (manifestUpdated) {
 | 
			
		||||
        console.log(`🔄 Updated src/manifest.json version to ${version}`);
 | 
			
		||||
    } else {
 | 
			
		||||
        console.log("ℹ️ src/manifest.json not found or no changes needed.");
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default syncVersion;
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user