Skip to content

Commit

Permalink
+ Added manifest detection
Browse files Browse the repository at this point in the history
  + Added exe name and shaka option
+ Reverted to old design
+ README update
  • Loading branch information
BuildTools committed Oct 30, 2024
1 parent 0dc47ef commit a1e7ff5
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 94 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# WidevineProxy2
An extension-based proxy for Widevine EME challenges and license messages. \
Modifies the challenge before it reaches the web player and obtains the decryption keys from the response.
Modifies the challenge before it reaches the web player and retrieves the decryption keys from the response.

## Features
+ User-friendly / GUI-based
+ Bypasses one-time tokens, hashes and license wrapping
+ Bypasses one-time tokens, hashes, and license wrapping
+ JavaScript native Widevine implementation
+ Supports Widevine Device files
+ Manifest V3 compliant
Expand Down Expand Up @@ -46,10 +46,10 @@ Now, open the extension, click `Choose File` and select your Widevine Device fil

### Remote CDM
If you don't already have a `remote.json` file, open the API URL in the browser (if provided) and save the response as `remote.json`. \
Now, open the extension, click `Choose remote.json` and select the json file provided by your API.
Now, open the extension, click `Choose remote.json` and select the JSON file provided by your API.


+ Select the type of device you're using in the top right hand corner
+ Select the type of device you're using in the top right-hand corner
+ The files are saved in the extension's `chrome.storage.sync` storage and will be synchronized across any browsers into which the user is signed in with their Google account.
+ The maximum number of Widevine devices is ~25 **OR** ~200 Remote CDMs
+ Check `Enabled` to activate the message interception and you're done.
Expand All @@ -62,7 +62,7 @@ Keys are saved:
> [!NOTE]
> The video will not play when the interception is active, as the Widevine CDM library isn't able to decrypt the Android CDM license.
+ Click the `+` button to expand the section to reveal the PSSH and keys.
+ Click the `+` button to expand the section to reveal the PSSH and keys.

## FAQ
> What if I'm unable to get the keys?
Expand Down
67 changes: 59 additions & 8 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,35 @@ import { RemoteCdm } from "./remote_cdm.js";

const { LicenseType, SignedMessage, LicenseRequest, License } = protobuf.roots.default.license_protocol;

let manifests = new Map();
let requests = new Map();
let sessions = new Map();
let logs = [];

chrome.webRequest.onBeforeSendHeaders.addListener(
function(details) {
if (details.method === "GET") {
if (!requests.has(details.url)) {
const headers = details.requestHeaders
.filter(item => !(
item.name.startsWith('sec-ch-ua') ||
item.name.startsWith('Sec-Fetch') ||
item.name.startsWith('Accept-') ||
item.name.startsWith('Host') ||
item.name === "Connection"
)).reduce((acc, item) => {
acc[item.name] = item.value;
return acc;
}, {});
console.log(headers);
requests.set(details.url, headers);
}
}
},
{urls: ["<all_urls>"]},
['requestHeaders', chrome.webRequest.OnSendHeadersOptions.EXTRA_HEADERS].filter(Boolean)
);

async function parseClearKey(body, sendResponse, tab_url) {
const clearkey = JSON.parse(atob(body));

Expand All @@ -35,13 +61,14 @@ async function parseClearKey(body, sendResponse, tab_url) {
return;
}

console.log("[WidevineProxy2]", "CLEARKEY KEYS", formatted_keys);
console.log("[WidevineProxy2]", "CLEARKEY KEYS", formatted_keys, tab_url);
const log = {
type: "CLEARKEY",
pssh_data: pssh_data,
keys: formatted_keys,
url: tab_url,
timestamp: Math.floor(Date.now() / 1000)
timestamp: Math.floor(Date.now() / 1000),
manifests: manifests.has(tab_url) ? manifests.get(tab_url) : []
}
logs.push(log);

Expand Down Expand Up @@ -95,7 +122,7 @@ async function parseLicense(body, sendResponse, tab_url) {
const signed_license_message = SignedMessage.decode(license);

if (signed_license_message.type !== SignedMessage.MessageType.LICENSE) {
console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString())
console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString());
sendResponse();
return;
}
Expand All @@ -118,7 +145,8 @@ async function parseLicense(body, sendResponse, tab_url) {
pssh_data: pssh,
keys: keys,
url: tab_url,
timestamp: Math.floor(Date.now() / 1000)
timestamp: Math.floor(Date.now() / 1000),
manifests: manifests.has(tab_url) ? manifests.get(tab_url) : []
}
logs.push(log);
await AsyncLocalStorage.setStorage({[pssh]: log});
Expand Down Expand Up @@ -173,7 +201,7 @@ async function parseLicenseRemote(body, sendResponse, tab_url) {
const signed_license_message = SignedMessage.decode(license);

if (signed_license_message.type !== SignedMessage.MessageType.LICENSE) {
console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString())
console.log("[WidevineProxy2]", "INVALID_MESSAGE_TYPE", signed_license_message.type.toString());
sendResponse();
return;
}
Expand All @@ -200,7 +228,7 @@ async function parseLicenseRemote(body, sendResponse, tab_url) {
await remote_cdm.parse_license(session_id.id, body);
const returned_keys = await remote_cdm.get_keys(session_id.id, "CONTENT");
await remote_cdm.close(session_id.id);

if (returned_keys.length === 0) {
sendResponse();
return;
Expand All @@ -214,7 +242,8 @@ async function parseLicenseRemote(body, sendResponse, tab_url) {
pssh_data: session_id.pssh,
keys: keys,
url: tab_url,
timestamp: Math.floor(Date.now() / 1000)
timestamp: Math.floor(Date.now() / 1000),
manifests: manifests.has(tab_url) ? manifests.get(tab_url) : []
}
logs.push(log);
await AsyncLocalStorage.setStorage({[session_id.pssh]: log});
Expand All @@ -225,10 +254,13 @@ async function parseLicenseRemote(body, sendResponse, tab_url) {

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
const tab_url = sender.tab ? sender.tab.url : null;

switch (message.type) {
case "REQUEST":
if (!await SettingsManager.getEnabled()) {
sendResponse(message.body);
manifests.clear();
return;
}

Expand All @@ -254,10 +286,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
case "RESPONSE":
if (!await SettingsManager.getEnabled()) {
sendResponse(message.body);
manifests.clear();
return;
}

const tab_url = sender.tab ? sender.tab.url : null;
try {
await parseClearKey(message.body, sendResponse, tab_url);
return;
Expand Down Expand Up @@ -294,7 +326,26 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
break;
case "CLEAR":
logs = [];
manifests.clear()
break;
case "MANIFEST":
const parsed = JSON.parse(message.body);
const element = {
type: parsed.type,
url: parsed.url,
headers: requests.has(parsed.url) ? requests.get(parsed.url) : [],
};

if (!manifests.has(tab_url)) {
manifests.set(tab_url, [element]);
} else {
let elements = manifests.get(tab_url);
if (!elements.some(e => e.url === parsed.url)) {
elements.push(element);
manifests.set(tab_url, elements);
}
}
sendResponse();
}
})();
return true;
Expand Down
119 changes: 118 additions & 1 deletion content_script.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ function uint8ArrayToBase64(uint8array) {
return btoa(String.fromCharCode.apply(null, uint8array));
}

function uint8ArrayToString(uint8array) {
return String.fromCharCode.apply(null, uint8array)
}

function base64toUint8Array(base64_string){
return Uint8Array.from(atob(base64_string), c => c.charCodeAt(0))
}
Expand All @@ -15,7 +19,6 @@ function compareUint8Arrays(arr1, arr2) {
function emitAndWaitForResponse(type, data) {
return new Promise((resolve) => {
const requestId = Math.random().toString(16).substring(2, 9);

const responseHandler = (event) => {
const { detail } = event;
if (detail.substring(0, 7) === requestId) {
Expand Down Expand Up @@ -47,6 +50,39 @@ function getEventListeners(type) {
return store[type];
}

class Evaluator {
static isDASH(text) {
return text.includes('<mpd') && text.includes('</mpd>');
}

static isHLS(text) {
return text.includes('#extm3u');
}

static isHLSMaster(text) {
return text.includes('#ext-x-stream-inf');
}

static isMSS(text) {
return text.includes('<smoothstreamingmedia') && text.includes('</smoothstreamingmedia>');
}

static getManifestType(text) {
const lower = text.toLowerCase();
if (this.isDASH(lower)) {
return "DASH";
} else if (this.isHLS(lower)) {
if (this.isHLSMaster(lower)) {
return "HLS_MASTER";
} else {
return "HLS_PLAYLIST";
}
} else if (this.isMSS(lower)) {
return "MSS";
}
}
}

(async () => {
if (typeof EventTarget !== 'undefined') {
proxy(EventTarget.prototype, 'addEventListener', async (_target, _this, _args) => {
Expand Down Expand Up @@ -147,3 +183,84 @@ function getEventListeners(type) {
});
}
})();

const originalFetch = window.fetch;
window.fetch = function() {
return new Promise(async (resolve, reject) => {
originalFetch.apply(this, arguments).then((response) => {
if (response) {
response.clone().text().then((text) => {
const manifest_type = Evaluator.getManifestType(text);
if (manifest_type) {
if (arguments.length === 1) {
emitAndWaitForResponse("MANIFEST", JSON.stringify({
"url": arguments[0].url,
"type": manifest_type,
}));
} else if (arguments.length === 2) {
emitAndWaitForResponse("MANIFEST", JSON.stringify({
"url": arguments[0],
"type": manifest_type,
}));
}
}
resolve(response);
}).catch(() => {
resolve(response);
})
} else {
resolve(response);
}
}).catch(() => {
resolve();
})
})
}

const open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
this._method = method;
return open.apply(this, arguments);
};

const send = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(postData) {
this.addEventListener('load', async function() {
if (this._method === "GET") {
let body = void 0;
switch (this.responseType) {
case "":
case "text":
body = this.responseText ?? this.response;
break;
case "json":
// TODO: untested
body = JSON.stringify(this.response);
break;
case "arraybuffer":
// TODO: untested
if (this.response.byteLength) {
const response = new Uint8Array(this.response);
body = uint8ArrayToString(new Uint8Array([...response.slice(0, 2000), ...response.slice(-2000)]));
}
break;
case "document":
// todo
break;
case "blob":
body = await this.response.text();
break;
}
if (body) {
const manifest_type = Evaluator.getManifestType(body);
if (manifest_type) {
emitAndWaitForResponse("MANIFEST", JSON.stringify({
"url": this.responseURL,
"type": manifest_type,
}));
}
}
}
});
return send.apply(this, arguments);
};
5 changes: 3 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"manifest_version": 3,
"name": "WidevineProxy2",
"version": "0.7.4",
"version": "0.8",
"permissions": [
"activeTab",
"tabs",
"storage",
"unlimitedStorage"
"unlimitedStorage",
"webRequest"
],
"host_permissions": [
"*://*/*"
Expand Down
Loading

0 comments on commit a1e7ff5

Please sign in to comment.