diff --git a/README.md b/README.md
index 16ce2fc..4cb2211 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,8 @@ Keep these extension core files inside `src`:
- `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`.
diff --git a/frontend/package.json b/frontend/package.json
index 326d96a..bae1047 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,30 +1,30 @@
{
- "name": "frontend",
- "private": true,
- "version": "2.1.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "vite build",
- "lint": "eslint .",
- "preview": "vite preview"
- },
- "dependencies": {
- "@tailwindcss/vite": "^4.1.11",
- "react": "^19.1.0",
- "react-dom": "^19.1.0",
- "react-router-dom": "^7.7.0",
- "tailwindcss": "^4.1.11"
- },
- "devDependencies": {
- "@eslint/js": "^9.31.0",
- "@types/react": "^19.1.8",
- "@types/react-dom": "^19.1.6",
- "@vitejs/plugin-react": "^4.7.0",
- "eslint": "^9.31.0",
- "eslint-plugin-react-hooks": "^5.2.0",
- "eslint-plugin-react-refresh": "^0.4.20",
- "globals": "^16.3.0",
- "vite": "^7.0.5"
- }
-}
\ No newline at end of file
+ "name": "frontend",
+ "private": true,
+ "version": "2.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.1.11",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-router-dom": "^7.7.0",
+ "tailwindcss": "^4.1.11"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.31.0",
+ "@types/react": "^19.1.8",
+ "@types/react-dom": "^19.1.6",
+ "@vitejs/plugin-react": "^4.7.0",
+ "eslint": "^9.31.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "globals": "^16.3.0",
+ "vite": "^7.0.5"
+ }
+}
diff --git a/mv2/background.js b/mv2/background.js
new file mode 100644
index 0000000..da9c10f
--- /dev/null
+++ b/mv2/background.js
@@ -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.");
+ }
+ });
+ }
+});
diff --git a/mv2/content.js b/mv2/content.js
new file mode 100644
index 0000000..55b7891
--- /dev/null
+++ b/mv2/content.js
@@ -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 or
+ (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,
+ });
+ }
+});
diff --git a/mv2/inject.js b/mv2/inject.js
new file mode 100644
index 0000000..fdcb029
--- /dev/null
+++ b/mv2/inject.js
@@ -0,0 +1,1122 @@
+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;
+ }
+});
+
+function safeHeaderShellEscape(str) {
+ return str
+ .replace(/\\/g, "\\\\")
+ .replace(/"/g, '\\"')
+ .replace(/\$/g, "\\$") // escape shell expansion
+ .replace(/`/g, "\\`")
+ .replace(/\n/g, ""); // strip newlines
+}
+
+// Intercep network to find manifest
+function injectManifestInterceptor() {
+ const script = document.createElement("script");
+ script.textContent = `
+ (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(" {
+ headersObj[key] = value;
+ });
+
+ const headerFlags = Object.entries(headersObj)
+ .map(([key, val]) => '--add-headers "' + safeHeaderShellEscape(key) + ': ' + safeHeaderShellEscape(val) + '"')
+ .join(" ");
+
+ window.postMessage({
+ type: "__MANIFEST_HEADERS__",
+ url,
+ headers: headerFlags
+ }, "*");
+ }
+ } 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)) {
+ window.postMessage({ type: "__MANIFEST_URL__", data: this.__url }, "*");
+ console.log("[Manifest][xhr]", this.__url, 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];
+ }
+ });
+
+ const headerFlags = Object.entries(xhrHeaders)
+ .map(([key, val]) => '--add-headers "' + safeHeaderShellEscape(key) + ': ' + safeHeaderShellEscape(val) + '"')
+ .join(" ");
+
+ window.postMessage({
+ type: "__MANIFEST_HEADERS__",
+ url: this.__url,
+ headers: headerFlags
+ }, "*");
+ }
+ } catch (e) {}
+ });
+ return originalXHRSend.apply(this, arguments);
+ };
+ })();
+ `;
+ document.documentElement.appendChild(script);
+ script.remove();
+}
+
+injectManifestInterceptor();
+
+// 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>/
+ );
+ 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);
+ };
+})();
diff --git a/mv2/manifest.json b/mv2/manifest.json
new file mode 100644
index 0000000..fb34e80
--- /dev/null
+++ b/mv2/manifest.json
@@ -0,0 +1,41 @@
+{
+ "manifest_version": 2,
+ "name": "CDRM Extension",
+ "version": "2.1.0",
+ "description": "Decrypt DRM protected content",
+ "permissions": [
+ "webRequest",
+ "webRequestBlocking",
+ "",
+ "activeTab",
+ "storage",
+ "tabs",
+ "contextMenus"
+ ],
+ "background": {
+ "scripts": ["background.js"],
+ "persistent": true
+ },
+ "content_scripts": [
+ {
+ "matches": [""],
+ "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"
+ }
+}
diff --git a/src/background.js b/src/background.js
index da9c10f..3c0fc5d 100644
--- a/src/background.js
+++ b/src/background.js
@@ -1,8 +1,7 @@
-// Open popout window when the extension icon is clicked
-chrome.browserAction.onClicked.addListener(() => {
+chrome.action.onClicked.addListener(() => {
chrome.windows.create({
url: chrome.runtime.getURL("index.html"),
- type: "popup", // opens as a floating window
+ type: "popup",
width: 800,
height: 600,
});
diff --git a/src/manifest.json b/src/manifest.json
index 00a5ab7..a2a5d0a 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -1,49 +1,37 @@
{
- "manifest_version": 2,
- "name": "CDRM Extension",
- "version": "2.1.0",
- "description": "Decrypt DRM protected content",
- "permissions": [
- "webRequest",
- "webRequestBlocking",
- "",
- "activeTab",
- "storage",
- "tabs",
- "contextMenus"
- ],
- "background": {
- "scripts": [
- "background.js"
+ "manifest_version": 3,
+ "name": "CDRM Extension",
+ "version": "2.1.0",
+ "description": "Decrypt DRM protected content",
+ "permissions": ["storage", "activeTab", "contextMenus"],
+ "host_permissions": [""],
+ "background": {
+ "service_worker": "background.js"
+ },
+ "content_scripts": [
+ {
+ "matches": [""],
+ "js": ["content.js"],
+ "run_at": "document_start",
+ "all_frames": true
+ }
],
- "persistent": true
- },
- "content_scripts": [
- {
- "matches": [
- ""
- ],
- "js": [
- "content.js"
- ],
- "run_at": "document_start",
- "all_frames": true
+ "web_accessible_resources": [
+ {
+ "resources": ["inject.js"],
+ "matches": [""]
+ }
+ ],
+ "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"
}
- ],
- "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"
- }
-}
\ No newline at end of file
+}