Add files via upload

This commit is contained in:
larley
2024-10-22 11:09:50 +02:00
committed by GitHub
parent fb4a4c0b03
commit fe216a4ac9
20 changed files with 13167 additions and 2 deletions
+67 -2
View File
@@ -1,2 +1,67 @@
# 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.
# 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.
## Features
+ User-friendly / GUI-based
+ Bypasses one-time tokens, hashes and license wrapping
+ JavaScript native Widevine implementation
+ Supports Widevine Device files
+ Manifest V3 compliant
## Widevine Devices
This addon requires a Widevine Device file to work, which is not provided by this project.
+ Follow [this](https://forum.videohelp.com/threads/408031) guide if you want to dump your own device.
+ Ready-to-use Widevine Devices can be found on the [VideoHelp forum](https://forum.videohelp.com/forums/48).
## Compatibility
+ Compatible (tested) browsers: Firefox/Chrome on Windows/Linux.
+ Works with any service that accepts challenges from Android devices on the same endpoint.
## Installation
+ Chrome
1. Download this repository as a ZIP file
2. Navigate to `chrome://extensions/`
3. Enable `Developer mode`
4. Click `Load unpacked` and the select downloaded file
+ Firefox
+ Persistent installation
1. Download the XPI file from the [releases section](https://github.com/DevLARLEY/WidevineProxy2/releases)
2. Navigate to `about:addons`
3. Click the settings icon and choose `Install Add-on From File...`
4. Select the downloaded file
+ Temporary installation
1. Download this repository as a ZIP file
2. Navigate to `about:debugging#/runtime/this-firefox`
3. Click `Load Temporary Add-on...` and select the downloaded file
## Setup
+ Once installed, open the extension, click `Choose File` and select your Widevine Device file.
+ 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.
+ Due to the sync storage limit of 100KB, the maximum amount of installable of devices at the same time is ~30.
+ Check `Enabled` to activate the message interception and you're done.
## Usage
All the user has to do is to play a DRM protected video and the decryption keys should appear in the `Keys` group box (if the service is not unsupported, as stated above). \
Keys are saved:
+ Temporarily until the extension is either refreshed manually (if installed temporarily) or a removal of the keys is manually initiated.
+ Permanently in the extension's `chrome.storage.local` storage until manually wiped or exported via the command line.
## Issues
+ DRM playback won't work when the extension is disabled and EME Logger is active. This caused by my fix for dealing with EME Logger interference (solutions are welcome).
## Demo
[Demo](demo.mp4)
## Disclaimer
+ This program is intended solely for educational purposes.
+ Do not use this program to decrypt or access any content for which you do not have the legal rights or explicit permission.
+ Unauthorized decryption or distribution of copyrighted materials is a violation of applicable laws and intellectual property rights.
+ This tool must not be used for any illegal activities, including but not limited to piracy, circumventing digital rights management (DRM), or unauthorized access to protected content.
+ The developers, contributors, and maintainers of this program are not responsible for any misuse or illegal activities performed using this software.
+ By using this program, you agree to comply with all applicable laws and regulations governing digital rights and copyright protections.
## Credits
+ [node-widevine](https://github.com/Frooastside/node-widevine)
+ [forge](https://github.com/digitalbazaar/forge)
+ [protobuf.js](https://github.com/protobufjs/protobuf.js)
+158
View File
@@ -0,0 +1,158 @@
import "./protobuf.min.js";
import "./license_protocol.js";
import "./forge.min.js";
import { Session } from "./license.js";
import { DeviceManager, base64toUint8Array, uint8ArrayToBase64, uint8ArrayToHex, SettingsManager, AsyncLocalStorage } from "./util.js";
import { WidevineDevice } from "./device.js";
const { LicenseType, SignedMessage, LicenseRequest, License } = protobuf.roots.default.license_protocol;
let sessions = new Map();
let logs = [];
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
(async () => {
switch (message.type) {
case "REQUEST":
if (!await SettingsManager.getEnabled()) {
sendResponse(message.body);
return;
}
try {
JSON.parse(atob(message.body));
sendResponse(message.body);
return;
} catch {
if (message.body) {
const signed_message = SignedMessage.decode(base64toUint8Array(message.body));
const license_request = LicenseRequest.decode(signed_message.msg);
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
if (!pssh_data) {
sendResponse(message.body); // TODO: send report message back or just log from background script
return;
}
if (logs.filter(log => log.pssh_data === uint8ArrayToBase64(pssh_data)).length > 0) {
console.log("[WidevineProxy2]", `KEYS_ALREADY_RETRIEVED: ${uint8ArrayToBase64(pssh_data)}`);
sendResponse(message.body);
return;
}
const selected_device_name = await DeviceManager.getSelectedWidevineDevice();
if (!selected_device_name) {
sendResponse(message.body);
return;
}
const device_b64 = await DeviceManager.loadWidevineDevice(selected_device_name);
const widevine_device = new WidevineDevice(base64toUint8Array(device_b64).buffer);
const private_key = `-----BEGIN RSA PRIVATE KEY-----${uint8ArrayToBase64(widevine_device.private_key)}-----END RSA PRIVATE KEY-----`;
const session = new Session(
{
privateKey: private_key,
identifierBlob: widevine_device.client_id_bytes
},
pssh_data
);
const [challenge, request_id] = session.createLicenseRequest(LicenseType.STREAMING, widevine_device.type === 2);
sessions.set(uint8ArrayToBase64(request_id), session);
sendResponse(uint8ArrayToBase64(challenge));
}
}
break;
case "RESPONSE":
if (!await SettingsManager.getEnabled()) {
sendResponse(message.body);
return;
}
const tab_url = sender.tab ? sender.tab.url : null;
try {
const clearkey = JSON.parse(atob(message.body));
const formatted_keys = clearkey["keys"].map(key => ({
...key,
kid: uint8ArrayToHex(base64toUint8Array(key.kid.replace(/-/g, "+").replace(/_/g, "/") + "==")),
k: uint8ArrayToHex(base64toUint8Array(key.k.replace(/-/g, "+").replace(/_/g, "/") + "=="))
}));
const pssh_data = btoa(JSON.stringify({kids: clearkey["keys"].map(key => key.k)}));
if (logs.filter(log => log.pssh_data === pssh_data).length > 0) {
console.log("[WidevineProxy2]", `KEYS_ALREADY_RETRIEVED: ${pssh_data}`);
sendResponse();
return;
}
console.log("keys", formatted_keys);
const log = {
type: "CLEARKEY",
pssh_data: pssh_data,
keys: formatted_keys,
url: tab_url,
timestamp: Math.floor(Date.now() / 1000)
}
logs.push(log);
await AsyncLocalStorage.setStorage({[pssh_data]: log});
sendResponse();
return;
} catch (e) {
const license = base64toUint8Array(message.body);
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())
sendResponse();
return;
}
const license_obj = License.decode(signed_license_message.msg);
const loaded_request_id = uint8ArrayToBase64(license_obj.id.requestId);
if (!sessions.has(loaded_request_id)) {
sendResponse();
return;
}
const loadedSession = sessions.get(loaded_request_id);
const keys = await loadedSession.parseLicense(license);
const pssh = loadedSession.getPSSH();
console.log("[WidevineProxy2]", "KEYS", JSON.stringify(keys), tab_url);
const log = {
type: "WIDEVINE",
pssh_data: pssh,
keys: keys,
url: tab_url,
timestamp: Math.floor(Date.now() / 1000)
}
logs.push(log);
await AsyncLocalStorage.setStorage({[pssh]: log});
sessions.delete(loaded_request_id);
sendResponse();
return;
}
case "GET_LOGS":
sendResponse(logs);
break;
case "OPEN_PICKER":
chrome.windows.create({
url: 'picker/filePicker.html',
type: 'popup',
width: 300,
height: 200,
});
break;
case "CLEAR":
logs = [];
break;
}
})();
return true;
});
+118
View File
@@ -0,0 +1,118 @@
export class AES_CMAC {
BLOCK_SIZE = 16;
XOR_RIGHT = new Uint8Array([
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87
]);
EMPTY_BLOCK_SIZE_BUFFER = new Uint8Array(this.BLOCK_SIZE);
constructor(key) {
if (![16, 24, 32].includes(key.length)) {
throw new Error("Key size must be 128, 192, or 256 bits.");
}
this._key = key;
}
async calculate(message) {
this._subkeys = await this._generateSubkeys();
const blockCount = this._getBlockCount(message);
let x = this.EMPTY_BLOCK_SIZE_BUFFER;
let y;
for (let i = 0; i < blockCount - 1; i++) {
const from = i * this.BLOCK_SIZE;
const block = message.subarray(from, from + this.BLOCK_SIZE);
y = this._xor(x, block);
x = await this._aes(y);
}
y = this._xor(x, this._getLastBlock(message));
x = await this._aes(y);
return x;
}
async _generateSubkeys() {
const l = await this._aes(this.EMPTY_BLOCK_SIZE_BUFFER);
let first = this._bitShiftLeft(l);
if (l[0] & 0x80) {
first = this._xor(first, this.XOR_RIGHT);
}
let second = this._bitShiftLeft(first);
if (first[0] & 0x80) {
second = this._xor(second, this.XOR_RIGHT);
}
return { first: first, second: second };
}
_getBlockCount(message) {
const blockCount = Math.ceil(message.length / this.BLOCK_SIZE);
return blockCount === 0 ? 1 : blockCount;
}
async _aes(message) {
const keyBuffer = forge.util.createBuffer(this._key, 'raw');
const cipher = forge.cipher.createCipher('AES-CBC', keyBuffer);
const iv = forge.util.createBuffer(new Uint8Array(16), 'raw');
cipher.start({
iv: iv.getBytes()
});
cipher.update(forge.util.createBuffer(message));
cipher.finish();
const outputBuffer = cipher.output;
return new Uint8Array(outputBuffer.getBytes().slice(0, 16).split('').map(c => c.charCodeAt(0)));
}
_getLastBlock(message) {
const blockCount = this._getBlockCount(message);
const paddedBlock = this._padding(message, blockCount - 1);
let complete = false;
if (message.length > 0) {
complete = message.length % this.BLOCK_SIZE === 0;
}
const key = complete ? this._subkeys.first : this._subkeys.second;
return this._xor(paddedBlock, key);
}
_padding(message, blockIndex) {
const block = new Uint8Array(this.BLOCK_SIZE);
const from = blockIndex * this.BLOCK_SIZE;
const slice = message.subarray(from, from + this.BLOCK_SIZE);
block.set(slice);
if (slice.length !== this.BLOCK_SIZE) {
block[slice.length] = 0x80;
}
return block;
}
_bitShiftLeft(input) {
const output = new Uint8Array(input.length);
let overflow = 0;
for (let i = input.length - 1; i >= 0; i--) {
output[i] = (input[i] << 1) | overflow;
overflow = input[i] & 0x80 ? 1 : 0;
}
return output;
}
_xor(a, b) {
const length = Math.min(a.length, b.length);
const output = new Uint8Array(length);
for (let i = 0; i < length; i++) {
output[i] = a[i] ^ b[i];
}
return output;
}
}
+149
View File
@@ -0,0 +1,149 @@
function uint8ArrayToBase64(uint8array) {
return btoa(String.fromCharCode.apply(null, uint8array));
}
function base64toUint8Array(base64_string){
return Uint8Array.from(atob(base64_string), c => c.charCodeAt(0))
}
function compareUint8Arrays(arr1, arr2) {
if (arr1.length !== arr2.length)
return false;
return Array.from(arr1).every((value, index) => value === arr2[index]);
}
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) {
document.removeEventListener('responseReceived', responseHandler);
resolve(detail.substring(7));
}
};
document.addEventListener('responseReceived', responseHandler);
const requestEvent = new CustomEvent('response', {
detail: {
type: type,
body: data,
requestId: requestId,
}
});
document.dispatchEvent(requestEvent);
});
}
const fnproxy = (object, func) => new Proxy(object, { apply: func });
const proxy = (object, key, func) => Object.hasOwnProperty.call(object, key) && Object.defineProperty(object, key, {
value: fnproxy(object[key], func)
});
function getEventListeners(type) {
if (this == null) return [];
const store = this[Symbol.for(getEventListeners)];
if (store == null || store[type] == null) return [];
return store[type];
}
(async () => {
if (typeof EventTarget !== 'undefined') {
proxy(EventTarget.prototype, 'addEventListener', async (_target, _this, _args) => {
if (_this != null) {
const [type, listener] = _args;
const storeKey = Symbol.for(getEventListeners);
if (!(storeKey in _this)) _this[storeKey] = {};
const store = _this[storeKey];
if (!(type in store)) store[type] = [];
const listeners = store[type];
let wrappedListener = listener;
if (!!listener && !listener._isWrapped) {
wrappedListener = async function(event) {
if (event instanceof MediaKeyMessageEvent) {
if (event._isCustomEvent) {
if (listener.handleEvent) {
listener.handleEvent(event);
} else {
listener.call(this, event);
}
return;
}
let newBody = new Uint8Array(event.message);
if (!compareUint8Arrays(new Uint8Array([0x08, 0x04]), new Uint8Array(event.message))) {
console.log("[WidevineProxy2]", "WIDEVINE_PROXY", "MESSAGE", listener);
if (listener.name !== "messageHandler") {
const oldChallenge = uint8ArrayToBase64(new Uint8Array(event.message));
const newChallenge = await emitAndWaitForResponse("REQUEST", oldChallenge);
if (oldChallenge !== newChallenge) {
// Playback will fail if the challenges are the same (aka. the background script
// returned the same challenge because the addon is disabled), but I still
// override the challenge anyway, so check beforehand (in base64 form)
newBody = base64toUint8Array(newChallenge);
}
} else {
// trick EME Logger
// better suggestions for avoiding EME Logger interference are welcome
await emitAndWaitForResponse("REQUEST", "");
}
}
const newEvent = new MediaKeyMessageEvent('message', {
isTrusted: event.isTrusted,
bubbles: event.bubbles,
cancelBubble: event.cancelBubble,
composed: event.composed,
currentTarget: event.currentTarget,
defaultPrevented: event.defaultPrevented,
eventPhase: event.eventPhase,
message: newBody.buffer,
messageType: event.messageType,
returnValue: event.returnValue,
srcElement: event.srcElement,
target: event.target,
timeStamp: event.timeStamp,
});
newEvent._isCustomEvent = true;
_this.dispatchEvent(newEvent);
event.stopImmediatePropagation();
return
}
if (listener.handleEvent) {
listener.handleEvent(event);
} else {
listener.call(this, event);
}
};
wrappedListener._isWrapped = true;
wrappedListener.originalListener = listener;
}
const alreadyAdded = listeners.some(
storedListener => storedListener && storedListener.originalListener === listener
);
if (!alreadyAdded) {
listeners.push(wrappedListener);
_args[1] = wrappedListener;
}
}
return _target.apply(_this, _args);
});
}
if (typeof MediaKeySession !== 'undefined') {
proxy(MediaKeySession.prototype, 'update', async (_target, _this, _args) => {
const [response] = _args;
console.log("[WidevineProxy2]", "WIDEVINE_PROXY", "UPDATE");
await emitAndWaitForResponse("RESPONSE", uint8ArrayToBase64(new Uint8Array(response)))
return await _target.apply(_this, _args);
});
}
})();
BIN
View File
Binary file not shown.
+65
View File
@@ -0,0 +1,65 @@
const { ClientIdentification } = protobuf.roots.default.license_protocol;
export class Crc32 {
constructor() {
this.crc_table = this.setupTable();
}
setupTable() {
let c;
const crcTable = [];
for (let n = 0; n < 256; n++) {
c = n;
for (let k = 0; k < 8; k++) {
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
}
crcTable[n] = c;
}
return crcTable;
}
crc32(uint8Array) {
let crc = 0 ^ (-1);
for (let i = 0; i < uint8Array.length; i++) {
crc = (crc >>> 8) ^ this.crc_table[(crc ^ uint8Array[i]) & 0xFF];
}
return (crc ^ (-1)) >>> 0;
}
}
export class WidevineDevice {
constructor(bytes) {
this._raw_bytes = new Uint8Array(bytes);
this._data_view = new DataView(bytes);
this.version = this._data_view.getUint8(3);
this.type = this._data_view.getUint8(4);
this.security_level = this._data_view.getUint8(5);
this.flags = this._data_view.getUint8(6);
this.private_key_len = this._data_view.getUint16(7);
this.private_key = this._raw_bytes.subarray(9, 9 + this.private_key_len);
this.client_id_len = this._data_view.getUint16(9 + this.private_key_len);
this.client_id_bytes = this._raw_bytes.subarray(11 + this.private_key_len, 11 + this.private_key_len + this.client_id_len);
this.client_id = ClientIdentification.decode(this.client_id_bytes);
}
get_name() {
const client_info = Object.fromEntries(this.client_id.clientInfo.map(item => [item.name, item.value]))
const type = this.type === 1 ? "CHROME" : `L${this.security_level}`
let name = `[${type}]`;
if (client_info["company_name"])
name += ` ${client_info["company_name"]}`;
if (client_info["model_name"])
name += `, ${client_info["model_name"]}`;
if (client_info["product_name"])
name += `, ${client_info["product_name"]}`;
const crc32 = new Crc32();
name += `, ${crc32.crc32(this._raw_bytes).toString(16)}`;
return name;
}
}
+2
View File
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+371
View File
@@ -0,0 +1,371 @@
import { AES_CMAC } from "./cmac.js"
import {
compareUint8Arrays,
uint8ArrayToHex,
uint8ArrayToString,
stringToUint8Array,
stringToHex,
base64toUint8Array, uint8ArrayToBase64
} from "./util.js"
const { ClientIdentification, DrmCertificate, EncryptedClientIdentification, License, LicenseRequest, LicenseType,
ProtocolVersion, SignedDrmCertificate, SignedMessage, WidevinePsshData } = protobuf.roots.default.license_protocol;
export const SERVICE_CERTIFICATE_CHALLENGE = new Uint8Array([0x08, 0x04]);
const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]);
const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68])
const WIDEVINE_ROOT_PUBLIC_KEY = new Uint8Array([
0x30, 0x82, 0x01, 0x8a, 0x02, 0x82, 0x01, 0x81, 0x00, 0xb4, 0xfe, 0x39, 0xc3, 0x65, 0x90, 0x03, 0xdb, 0x3c, 0x11, 0x97, 0x09, 0xe8, 0x68, 0xcd,
0xf2, 0xc3, 0x5e, 0x9b, 0xf2, 0xe7, 0x4d, 0x23, 0xb1, 0x10, 0xdb, 0x87, 0x65, 0xdf, 0xdc, 0xfb, 0x9f, 0x35, 0xa0, 0x57, 0x03, 0x53, 0x4c, 0xf6,
0x6d, 0x35, 0x7d, 0xa6, 0x78, 0xdb, 0xb3, 0x36, 0xd2, 0x3f, 0x9c, 0x40, 0xa9, 0x95, 0x26, 0x72, 0x7f, 0xb8, 0xbe, 0x66, 0xdf, 0xc5, 0x21, 0x98,
0x78, 0x15, 0x16, 0x68, 0x5d, 0x2f, 0x46, 0x0e, 0x43, 0xcb, 0x8a, 0x84, 0x39, 0xab, 0xfb, 0xb0, 0x35, 0x80, 0x22, 0xbe, 0x34, 0x23, 0x8b, 0xab,
0x53, 0x5b, 0x72, 0xec, 0x4b, 0xb5, 0x48, 0x69, 0x53, 0x3e, 0x47, 0x5f, 0xfd, 0x09, 0xfd, 0xa7, 0x76, 0x13, 0x8f, 0x0f, 0x92, 0xd6, 0x4c, 0xdf,
0xae, 0x76, 0xa9, 0xba, 0xd9, 0x22, 0x10, 0xa9, 0x9d, 0x71, 0x45, 0xd6, 0xd7, 0xe1, 0x19, 0x25, 0x85, 0x9c, 0x53, 0x9a, 0x97, 0xeb, 0x84, 0xd7,
0xcc, 0xa8, 0x88, 0x82, 0x20, 0x70, 0x26, 0x20, 0xfd, 0x7e, 0x40, 0x50, 0x27, 0xe2, 0x25, 0x93, 0x6f, 0xbc, 0x3e, 0x72, 0xa0, 0xfa, 0xc1, 0xbd,
0x29, 0xb4, 0x4d, 0x82, 0x5c, 0xc1, 0xb4, 0xcb, 0x9c, 0x72, 0x7e, 0xb0, 0xe9, 0x8a, 0x17, 0x3e, 0x19, 0x63, 0xfc, 0xfd, 0x82, 0x48, 0x2b, 0xb7,
0xb2, 0x33, 0xb9, 0x7d, 0xec, 0x4b, 0xba, 0x89, 0x1f, 0x27, 0xb8, 0x9b, 0x88, 0x48, 0x84, 0xaa, 0x18, 0x92, 0x0e, 0x65, 0xf5, 0xc8, 0x6c, 0x11,
0xff, 0x6b, 0x36, 0xe4, 0x74, 0x34, 0xca, 0x8c, 0x33, 0xb1, 0xf9, 0xb8, 0x8e, 0xb4, 0xe6, 0x12, 0xe0, 0x02, 0x98, 0x79, 0x52, 0x5e, 0x45, 0x33,
0xff, 0x11, 0xdc, 0xeb, 0xc3, 0x53, 0xba, 0x7c, 0x60, 0x1a, 0x11, 0x3d, 0x00, 0xfb, 0xd2, 0xb7, 0xaa, 0x30, 0xfa, 0x4f, 0x5e, 0x48, 0x77, 0x5b,
0x17, 0xdc, 0x75, 0xef, 0x6f, 0xd2, 0x19, 0x6d, 0xdc, 0xbe, 0x7f, 0xb0, 0x78, 0x8f, 0xdc, 0x82, 0x60, 0x4c, 0xbf, 0xe4, 0x29, 0x06, 0x5e, 0x69,
0x8c, 0x39, 0x13, 0xad, 0x14, 0x25, 0xed, 0x19, 0xb2, 0xf2, 0x9f, 0x01, 0x82, 0x0d, 0x56, 0x44, 0x88, 0xc8, 0x35, 0xec, 0x1f, 0x11, 0xb3, 0x24,
0xe0, 0x59, 0x0d, 0x37, 0xe4, 0x47, 0x3c, 0xea, 0x4b, 0x7f, 0x97, 0x31, 0x1c, 0x81, 0x7c, 0x94, 0x8a, 0x4c, 0x7d, 0x68, 0x15, 0x84, 0xff, 0xa5,
0x08, 0xfd, 0x18, 0xe7, 0xe7, 0x2b, 0xe4, 0x47, 0x27, 0x12, 0x11, 0xb8, 0x23, 0xec, 0x58, 0x93, 0x3c, 0xac, 0x12, 0xd2, 0x88, 0x6d, 0x41, 0x3d,
0xc5, 0xfe, 0x1c, 0xdc, 0xb9, 0xf8, 0xd4, 0x51, 0x3e, 0x07, 0xe5, 0x03, 0x6f, 0xa7, 0x12, 0xe8, 0x12, 0xf7, 0xb5, 0xce, 0xa6, 0x96, 0x55, 0x3f,
0x78, 0xb4, 0x64, 0x82, 0x50, 0xd2, 0x33, 0x5f, 0x91, 0x02, 0x03, 0x01, 0x00, 0x01
]);
const STAGING_PRIVACY_CERT = new Uint8Array([
0x0a, 0xbf, 0x02, 0x08, 0x03, 0x12, 0x10, 0x28, 0x70, 0x34, 0x54, 0xc0, 0x08, 0xf6, 0x36, 0x18, 0xad, 0xe7, 0x44, 0x3d, 0xb6, 0xc4, 0xc8, 0x18,
0x8b, 0xe7, 0xf9, 0x90, 0x05, 0x22, 0x8e, 0x02, 0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01, 0x00, 0xb5, 0x21, 0x12, 0xb8, 0xd0, 0x5d, 0x02,
0x3f, 0xcc, 0x5d, 0x95, 0xe2, 0xc2, 0x51, 0xc1, 0xc6, 0x49, 0xb4, 0x17, 0x7c, 0xd8, 0xd2, 0xbe, 0xef, 0x35, 0x5b, 0xb0, 0x67, 0x43, 0xde, 0x66,
0x1e, 0x3d, 0x2a, 0xbc, 0x31, 0x82, 0xb7, 0x99, 0x46, 0xd5, 0x5f, 0xdc, 0x08, 0xdf, 0xe9, 0x54, 0x07, 0x81, 0x5e, 0x9a, 0x62, 0x74, 0xb3, 0x22,
0xa2, 0xc7, 0xf5, 0xe0, 0x67, 0xbb, 0x5f, 0x0a, 0xc0, 0x7a, 0x89, 0xd4, 0x5a, 0xea, 0x94, 0xb2, 0x51, 0x6f, 0x07, 0x5b, 0x66, 0xef, 0x81, 0x1d,
0x0d, 0x26, 0xe1, 0xb9, 0xa6, 0xb8, 0x94, 0xf2, 0xb9, 0x85, 0x79, 0x62, 0xaa, 0x17, 0x1c, 0x4f, 0x66, 0x63, 0x0d, 0x3e, 0x4c, 0x60, 0x27, 0x18,
0x89, 0x7f, 0x5e, 0x1e, 0xf9, 0xb6, 0xaa, 0xf5, 0xad, 0x4d, 0xba, 0x2a, 0x7e, 0x14, 0x17, 0x6d, 0xf1, 0x34, 0xa1, 0xd3, 0x18, 0x5b, 0x5a, 0x21,
0x8a, 0xc0, 0x5a, 0x4c, 0x41, 0xf0, 0x81, 0xef, 0xff, 0x80, 0xa3, 0xa0, 0x40, 0xc5, 0x0b, 0x09, 0xbb, 0xc7, 0x40, 0xee, 0xdc, 0xd8, 0xf1, 0x4d,
0x67, 0x5a, 0x91, 0x98, 0x0f, 0x92, 0xca, 0x7d, 0xdc, 0x64, 0x6a, 0x06, 0xad, 0xad, 0x51, 0x01, 0xf7, 0x4a, 0x0e, 0x49, 0x8c, 0xc0, 0x1f, 0x00,
0x53, 0x2b, 0xac, 0x21, 0x78, 0x50, 0xbd, 0x90, 0x5e, 0x90, 0x92, 0x36, 0x56, 0xb7, 0xdf, 0xef, 0xef, 0x42, 0x48, 0x67, 0x67, 0xf3, 0x3e, 0xf6,
0x28, 0x3d, 0x4f, 0x42, 0x54, 0xab, 0x72, 0x58, 0x93, 0x90, 0xbe, 0xe5, 0x58, 0x08, 0xf1, 0xd6, 0x68, 0x08, 0x0d, 0x45, 0xd8, 0x93, 0xc2, 0xbc,
0xa2, 0xf7, 0x4d, 0x60, 0xa0, 0xc0, 0xd0, 0xa0, 0x99, 0x3c, 0xef, 0x01, 0x60, 0x47, 0x03, 0x33, 0x4c, 0x36, 0x38, 0x13, 0x94, 0x86, 0xbc, 0x9d,
0xaf, 0x24, 0xfd, 0x67, 0xa0, 0x7f, 0x9a, 0xd9, 0x43, 0x02, 0x03, 0x01, 0x00, 0x01, 0x3a, 0x12, 0x73, 0x74, 0x61, 0x67, 0x69, 0x6e, 0x67, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x12, 0x80, 0x03, 0x98, 0x3e, 0x30, 0x35, 0x26, 0x75, 0xf4, 0x0b, 0xa7, 0x15, 0xfc,
0x24, 0x9b, 0xda, 0xe5, 0xd4, 0xac, 0x72, 0x49, 0xa2, 0x66, 0x65, 0x21, 0xe4, 0x36, 0x55, 0x73, 0x95, 0x29, 0x72, 0x1f, 0xf8, 0x80, 0xe0, 0xaa,
0xef, 0xc5, 0xe2, 0x7b, 0xc9, 0x80, 0xda, 0xea, 0xda, 0xbf, 0x3f, 0xc3, 0x86, 0xd0, 0x84, 0xa0, 0x2c, 0x82, 0x53, 0x78, 0x48, 0xcc, 0x75, 0x3f,
0xf4, 0x97, 0xb0, 0x11, 0xa7, 0xda, 0x97, 0x78, 0x8a, 0x00, 0xe2, 0xaa, 0x6b, 0x84, 0xcd, 0x7d, 0x71, 0xc0, 0x7a, 0x48, 0xeb, 0xf6, 0x16, 0x02,
0xcc, 0xa5, 0xa3, 0xf3, 0x20, 0x30, 0xa7, 0x29, 0x5c, 0x30, 0xda, 0x91, 0x5b, 0x91, 0xdc, 0x18, 0xb9, 0xbc, 0x95, 0x93, 0xb8, 0xde, 0x8b, 0xb5,
0x0f, 0x0d, 0xed, 0xc1, 0x29, 0x38, 0xb8, 0xe9, 0xe0, 0x39, 0xcd, 0xde, 0x18, 0xfa, 0x82, 0xe8, 0x1b, 0xb0, 0x32, 0x63, 0x0f, 0xe9, 0x55, 0xd8,
0x5a, 0x56, 0x6c, 0xe1, 0x54, 0x30, 0x0b, 0xf6, 0xd4, 0xc1, 0xbd, 0x12, 0x69, 0x66, 0x35, 0x6b, 0x28, 0x7d, 0x65, 0x7b, 0x18, 0xce, 0x63, 0xd0,
0xef, 0xd4, 0x5f, 0xc5, 0x26, 0x9e, 0x97, 0xea, 0xb1, 0x1c, 0xb5, 0x63, 0xe5, 0x56, 0x43, 0xb2, 0x6f, 0xf4, 0x9f, 0x10, 0x9c, 0x21, 0x01, 0xaf,
0xca, 0xf3, 0x5b, 0x83, 0x2f, 0x28, 0x8f, 0x0d, 0x9d, 0x45, 0x96, 0x0e, 0x25, 0x9e, 0x85, 0xfb, 0x5d, 0x24, 0xdb, 0xd2, 0xcf, 0x82, 0x76, 0x4c,
0x5d, 0xd9, 0xbf, 0x72, 0x7e, 0xfb, 0xe9, 0xc8, 0x61, 0xf8, 0x69, 0x32, 0x1f, 0x6a, 0xde, 0x18, 0x90, 0x5f, 0x4d, 0x92, 0xf9, 0xa6, 0xda, 0x65,
0x36, 0xdb, 0x84, 0x75, 0x87, 0x1d, 0x16, 0x8e, 0x87, 0x0b, 0xb2, 0x30, 0x3c, 0xf7, 0x0c, 0x6e, 0x97, 0x84, 0xc9, 0x3d, 0x2d, 0xe8, 0x45, 0xad,
0x82, 0x62, 0xbe, 0x7e, 0x0d, 0x4e, 0x2e, 0x4a, 0x07, 0x59, 0xce, 0xf8, 0x2d, 0x10, 0x9d, 0x25, 0x92, 0xc7, 0x24, 0x29, 0xf8, 0xc0, 0x17, 0x42,
0xba, 0xe2, 0xb3, 0xde, 0xca, 0xdb, 0xc3, 0x3c, 0x3e, 0x5f, 0x4b, 0xaf, 0x5e, 0x16, 0xec, 0xb7, 0x4e, 0xad, 0xba, 0xfc, 0xb7, 0xc6, 0x70, 0x5f,
0x7a, 0x9e, 0x3b, 0x6f, 0x39, 0x40, 0x38, 0x3f, 0x9c, 0x51, 0x16, 0xd2, 0x02, 0xa2, 0x0c, 0x92, 0x29, 0xee, 0x96, 0x9c, 0x25, 0x19, 0x71, 0x83,
0x03, 0xb5, 0x0d, 0x01, 0x30, 0xc3, 0x35, 0x2e, 0x06, 0xb0, 0x14, 0xd8, 0x38, 0x54, 0x0f, 0x8a, 0x0c, 0x22, 0x7c, 0x00, 0x11, 0xe0, 0xf5, 0xb3,
0x8e, 0x4e, 0x29, 0x8e, 0xd2, 0xcb, 0x30, 0x1e, 0xb4, 0x56, 0x49, 0x65, 0xf5, 0x5c, 0x5d, 0x79, 0x75, 0x7a, 0x25, 0x0a, 0x4e, 0xb9, 0xc8, 0x4a,
0xb3, 0xe6, 0x53, 0x9f, 0x6b, 0x6f, 0xdf, 0x56, 0x89, 0x9e, 0xa2, 0x99, 0x14
])
export class Session {
constructor(contentDecryptionModule, pssh) {
this._devicePrivateKey = forge.pki.privateKeyFromPem(
contentDecryptionModule.privateKey
)
this._identifierBlob = ClientIdentification.decode(
contentDecryptionModule.identifierBlob
)
if (typeof pssh === "string") {
pssh = base64toUint8Array(pssh)
}
this._pssh = pssh
}
async setDefaultServiceCertificate() {
await this.setServiceCertificate(STAGING_PRIVACY_CERT)
}
async setServiceCertificateFromMessage(rawSignedMessage) {
const signedMessage = SignedMessage.decode(new Uint8Array(rawSignedMessage))
if (!signedMessage.msg) {
throw new Error(
"the service certificate message does not contain a message"
)
}
await this.setServiceCertificate(signedMessage.msg)
}
async setServiceCertificate(serviceCertificate) {
const signedServiceCertificate = SignedDrmCertificate.decode(
serviceCertificate
)
if (!(await this._verifyServiceCertificate(signedServiceCertificate))) {
throw new Error(
"Service certificate is not signed by the Widevine root certificate"
)
}
this._serviceCertificate = signedServiceCertificate
}
createLicenseRequest(licenseType = LicenseType.STREAMING, android = false) {
if (compareUint8Arrays(this._pssh.subarray(4, 8), PSSH_MAGIC)) {
if (compareUint8Arrays(this._pssh.subarray(12, 28), WIDEVINE_SYSTEM_ID)) {
this._pssh = this._pssh.subarray(32)
} else {
throw new Error(`Invalid System ID in PSSH Box: ${uint8ArrayToHex(this._pssh.subarray(12, 28))}`)
}
}
const pssh = this._parseWidevinePsshData(this._pssh)
if (!pssh) {
throw new Error("Invalid WidevinePsshData found")
}
const request_id = android ? this._generateAndroidIdentifier() : this._generateGenericIdentifier();
const licenseRequest = new LicenseRequest({
type: LicenseRequest.RequestType.NEW,
contentId: new LicenseRequest.ContentIdentification({
widevinePsshData: new LicenseRequest.ContentIdentification.WidevinePsshData({
psshData: [this._pssh],
licenseType: licenseType,
requestId: request_id
})
}),
requestTime: Math.floor(Date.now() / 1000), // BigInt(Date.now()) / BigInt(1000),
protocolVersion: ProtocolVersion.VERSION_2_1,
keyControlNonce: Math.floor(Math.random() * 2 ** 31)
})
if (this._serviceCertificate) {
licenseRequest.encryptedClientId = this._encryptClientIdentification(
this._identifierBlob,
this._serviceCertificate
)
} else {
licenseRequest.clientId = this._identifierBlob
}
this._rawLicenseRequest = LicenseRequest.encode(licenseRequest).finish()
const pss = forge.pss.create({
md: forge.md.sha1.create(),
mgf: forge.mgf.mgf1.create(forge.md.sha1.create()),
saltLength: 20
})
const md = forge.md.sha1.create()
md.update(uint8ArrayToString(this._rawLicenseRequest), "raw")
const signature = stringToUint8Array(this._devicePrivateKey.sign(md, pss))
const signedLicenseRequest = new SignedMessage({
type: SignedMessage.MessageType.LICENSE_REQUEST,
msg: this._rawLicenseRequest,
signature: signature
})
return [SignedMessage.encode(signedLicenseRequest).finish(), request_id]
}
async parseLicense(rawLicense) {
if (!this._rawLicenseRequest) {
throw new Error("please request a license first")
}
const signedLicense = SignedMessage.decode(new Uint8Array(rawLicense))
if (!signedLicense.sessionKey) {
throw new Error("the license does not contain a session key")
}
if (!signedLicense.msg) {
throw new Error("the license does not contain a message")
}
if (!signedLicense.signature) {
throw new Error("the license does not contain a signature")
}
const sessionKey = this._devicePrivateKey.decrypt(
uint8ArrayToString(signedLicense.sessionKey),
"RSA-OAEP",
{
md: forge.md.sha1.create()
}
)
const cmac = new AES_CMAC(stringToUint8Array(sessionKey))
const encKeyBase = new Uint8Array([
...stringToUint8Array("ENCRYPTION"),
...new Uint8Array([0x00]),
...this._rawLicenseRequest,
...new Uint8Array([0x00, 0x00, 0x00, 0x80])
])
const authKeyBase = new Uint8Array([
...stringToUint8Array("AUTHENTICATION"),
...new Uint8Array([0x00]),
...this._rawLicenseRequest,
...new Uint8Array([0x00, 0x00, 0x02, 0x00])
])
const encKey = await cmac.calculate(
new Uint8Array([
...new Uint8Array([0x01]),
...encKeyBase
])
)
const server_key_1 = await cmac.calculate(new Uint8Array([
...new Uint8Array([0x01]),
...authKeyBase
]))
const server_key_2 = await cmac.calculate(new Uint8Array([
...new Uint8Array([0x02]),
...authKeyBase
]))
const serverKey = new Uint8Array([
...new Uint8Array(server_key_1),
...new Uint8Array(server_key_2)
])
const hmac = forge.hmac.create()
hmac.start(forge.md.sha256.create(), uint8ArrayToString(serverKey), "raw")
hmac.update(uint8ArrayToString(signedLicense.msg))
const calculatedSignature = stringToUint8Array(hmac.digest().data)
if (!compareUint8Arrays(calculatedSignature, signedLicense.signature)) {
throw new Error("signatures do not match")
}
const license = License.decode(signedLicense.msg)
const keyContainers = license.key.map(keyContainer => {
if (keyContainer.type && keyContainer.type === 2 && keyContainer.key && keyContainer.iv) {
const keyBuffer = forge.util.createBuffer(encKey, 'raw');
const decipher = forge.cipher.createDecipher(
"AES-CBC",
keyBuffer
)
decipher.start({
iv: uint8ArrayToString(keyContainer.iv)
})
decipher.update(forge.util.createBuffer(keyContainer.key))
decipher.finish()
return {
kid: keyContainer.id.length !== 0 ? uint8ArrayToHex(keyContainer.id) : "00000000000000000000000000000000",
k: stringToHex(decipher.output.data)
}
}
})
const valid_containers = keyContainers.filter(container => !!container);
if (valid_containers.length < 1) {
throw new Error("there was not a single valid key in the response")
}
return valid_containers;
}
_encryptClientIdentification(clientIdentification, signedServiceCertificate) {
if (!signedServiceCertificate.drmCertificate) {
throw new Error(
"the service certificate does not contain an actual certificate"
)
}
const serviceCertificate = DrmCertificate.decode(
signedServiceCertificate.drmCertificate
)
console.log("[WidevineProxy2]", "SERVICE_CERTIFICATE", serviceCertificate);
if (!serviceCertificate.publicKey) {
throw new Error("the service certificate does not contain a public key")
}
const key = forge.random.getBytesSync(16)
const iv = forge.random.getBytesSync(16)
const cipher = forge.cipher.createCipher("AES-CBC", key)
cipher.start({
iv: iv
})
cipher.update(
forge.util.createBuffer(
ClientIdentification.encode(clientIdentification).finish()
)
)
cipher.finish()
const rawEncryptedClientIdentification = stringToUint8Array(cipher.output.data)
const publicKey = forge.pki.publicKeyFromAsn1(
forge.asn1.fromDer(
uint8ArrayToString(serviceCertificate.publicKey)
)
)
const encryptedKey = publicKey.encrypt(key, "RSA-OAEP", {
md: forge.md.sha1.create()
})
return new EncryptedClientIdentification({
encryptedClientId: rawEncryptedClientIdentification,
encryptedClientIdIv: stringToUint8Array(iv),
encryptedPrivacyKey: stringToUint8Array(encryptedKey),
providerId: serviceCertificate.providerId,
serviceCertificateSerialNumber: serviceCertificate.serialNumber
})
}
async _verifyServiceCertificate(signedServiceCertificate) {
if (!signedServiceCertificate.drmCertificate) {
throw new Error(
"the service certificate does not contain an actual certificate"
)
}
if (!signedServiceCertificate.signature) {
throw new Error("the service certificate does not contain a signature")
}
const pss = forge.pss.create({
md: forge.md.sha1.create(),
mgf: forge.mgf.mgf1.create(forge.md.sha1.create()),
saltLength: 20
})
const sha1 = forge.md.sha1.create()
sha1.update(
uint8ArrayToString(signedServiceCertificate.drmCertificate),
"raw"
)
const publicKey = forge.pki.publicKeyFromAsn1(
forge.asn1.fromDer(
uint8ArrayToString(WIDEVINE_ROOT_PUBLIC_KEY)
)
)
return publicKey.verify(
sha1.digest().bytes(),
uint8ArrayToString(signedServiceCertificate.signature),
pss
)
}
_parseWidevinePsshData(pssh) {
try {
return WidevinePsshData.decode(pssh)
} catch {
return null
}
}
_generateAndroidIdentifier() {
return stringToUint8Array(`${forge.util.bytesToHex(forge.random.getBytesSync(8))}0100000000000000`)
}
_generateGenericIdentifier() {
return forge.random.getBytesSync(16)
}
getPSSH() {
return uint8ArrayToBase64(this._pssh);
}
}
+11727
View File
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
{
"manifest_version": 3,
"name": "WidevineProxy2",
"version": "0.2",
"permissions": [
"activeTab",
"tabs",
"storage",
"unlimitedStorage"
],
"host_permissions": [
"*://*/*"
],
"action": {
"default_popup": "panel/panel.html",
"default_icon": {
"128": "images/icon-128.png"
}
},
"icons": {
"128": "images/icon-128.png"
},
"background": {
"scripts": ["background.js"],
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["message_proxy.js"],
"run_at": "document_start",
"world": "ISOLATED",
"all_frames": true
},
{
"matches": ["<all_urls>"],
"js": ["content_script.js"],
"run_at": "document_start",
"world": "MAIN",
"all_frames": true
}
],
"browser_specific_settings": {
"gecko": {
"id": "DevLARLEY@larley.dev",
"strict_min_version": "58.0"
}
}
}
+22
View File
@@ -0,0 +1,22 @@
async function processMessage(detail) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({
type: detail.type,
body: detail.body,
}, (response) => {
if (chrome.runtime.lastError) {
return reject(chrome.runtime.lastError);
}
resolve(response);
});
})
}
document.addEventListener('response', async (event) => {
const { detail } = event;
const responseData = await processMessage(detail);
const responseEvent = new CustomEvent('responseReceived', {
detail: detail.requestId.concat(responseData)
});
document.dispatchEvent(responseEvent);
});
+53
View File
@@ -0,0 +1,53 @@
.header {
display: flex;
justify-content: center;
align-items: center;
}
.header > * {
margin: 5px;
}
#wvd > * {
margin: 5px;
}
.log-container {
display: flex;
justify-content: center;
}
.right-bound {
text-align: right;
}
.text-box {
width: 80%;
}
.expandableDiv {
width: 100%;
overflow: hidden;
background-color: lightblue;
padding: 0;
}
.expandableDiv.expanded {
padding: 5px;
}
.expandableDiv.collapsed {
padding: 0;
}
.always-visible {
display: block;
}
.expanded-only {
display: none;
}
.expandableDiv.expanded .expanded-only {
display: block;
}
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<script type="module" src="panel.js" defer></script>
<link rel="stylesheet" href="panel.css">
</head>
<body style="min-width: 200px;">
<div class="header">
<img src="../images/icon-128.png" alt="WidevineProxy Icon" width="64"/>
<img src="../images/proxy_text.png" alt="ProxyText" width="200"/>
</div>
<fieldset>
<legend>Settings</legend>
<input type="checkbox" id="enabled">
<label for="enabled"> Enabled</label><br>
<fieldset id="wvd">
<legend>Widevine Device</legend>
<button type="button" id="fileInput">Choose File</button><br>
<label>
<select id="wvd-combobox"></select>
</label><br>
<button type="button" id="remove">Remove</button>
<button type="button" id="download">Download</button>
</fieldset>
</fieldset>
<fieldset>
<legend>Keys</legend>
<button type="button" id="clear">Clear</button>
<div id="key-container"></div>
</fieldset>
</body>
</html>
+114
View File
@@ -0,0 +1,114 @@
import "../protobuf.min.js";
import "../license_protocol.js";
import { base64toUint8Array, DeviceManager, SettingsManager } from "../util.js";
const key_container = document.getElementById('key-container');
const enabled = document.getElementById('enabled');
enabled.addEventListener('change', async function (){
await SettingsManager.setEnabled(enabled.checked);
});
const wvd_combobox = document.getElementById('wvd-combobox');
wvd_combobox.addEventListener('change', async function() {
await DeviceManager.saveSelectedWidevineDevice(wvd_combobox.options[wvd_combobox.selectedIndex].text);
});
const remove = document.getElementById('remove');
remove.addEventListener('click', async function() {
await DeviceManager.removeSelectedWidevineDevice();
wvd_combobox.innerHTML = '';
await DeviceManager.loadSetAllWidevineDevices();
const selected_option = wvd_combobox.options[wvd_combobox.selectedIndex];
if (selected_option) {
await DeviceManager.saveSelectedWidevineDevice(selected_option.text);
}
});
const download = document.getElementById('download');
download.addEventListener('click', async function() {
const widevine_device = await DeviceManager.getSelectedWidevineDevice();
SettingsManager.downloadFile(
base64toUint8Array(await DeviceManager.loadWidevineDevice(widevine_device)),
widevine_device + ".wvd"
)
});
const clear = document.getElementById('clear');
clear.addEventListener('click', async function() {
chrome.runtime.sendMessage({ type: "CLEAR" });
key_container.innerHTML = "";
});
document.getElementById('fileInput').addEventListener('click', () => {
chrome.runtime.sendMessage({ type: "OPEN_PICKER" });
window.close();
});
function appendLog(result) {
const key_string = result.keys.map(key => `--key ${key.kid}:${key.k}`).join(' ');
const date = new Date(result.timestamp * 1000);
const date_string = date.toLocaleString();
key_container.innerHTML += `
<div class="log-container">
<button class="toggleButton">+</button>
<div class="expandableDiv collapsed">
<label class="always-visible right-bound">
URL:<input id="url" type="text" class="text-box" value="${result.url}">
</label>
<label class="expanded-only right-bound">
PSSH:<input id="pssh" type="text" class="text-box" value="${result.pssh_data}">
</label>
<label class="expanded-only right-bound">
Keys:<input id="keys" type="text" class="text-box" value="${key_string}">
</label>
<label class="expanded-only right-bound">
Date:<input id="date" type="text" class="text-box" value="${date_string}">
</label>
</div>
</div>`
}
function applyListeners() {
const toggleButtons = document.querySelectorAll('.toggleButton');
toggleButtons.forEach(button => {
button.addEventListener('click', function () {
const expandableDiv = this.nextElementSibling;
if (expandableDiv.classList.contains('collapsed')) {
expandableDiv.classList.remove('collapsed');
expandableDiv.classList.add('expanded');
} else {
expandableDiv.classList.remove('expanded');
expandableDiv.classList.add('collapsed');
}
});
});
}
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'local') {
for (const [key, values] of Object.entries(changes)) {
appendLog(values.newValue);
applyListeners();
}
}
});
function checkLogs() {
chrome.runtime.sendMessage({ type: "GET_LOGS" }, function(response) {
if (response) {
response.forEach((result) => {
appendLog(result);
});
applyListeners();
}
});
}
document.addEventListener('DOMContentLoaded', async function () {
enabled.checked = await SettingsManager.getEnabled();
await DeviceManager.loadSetAllWidevineDevices();
await DeviceManager.selectWidevineDevice(await DeviceManager.getSelectedWidevineDevice());
checkLogs();
});
+7
View File
@@ -0,0 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<body>
<input type="file" id="fileInput" accept=".wvd"/>
<script type="module" src="filePicker.js" defer></script>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
import "../protobuf.min.js";
import "../license_protocol.js";
import { SettingsManager } from "../util.js";
document.getElementById('fileInput').addEventListener('change', async (event) => {
const file = event.target.files[0];
await SettingsManager.importDevice(file).then(() => {
window.close();
});
});
+8
View File
File diff suppressed because one or more lines are too long
+213
View File
@@ -0,0 +1,213 @@
import { WidevineDevice } from "./device.js";
export class AsyncSyncStorage {
static async setStorage(items) {
return new Promise((resolve, reject) => {
chrome.storage.sync.set(items, () => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve();
}
});
});
}
static async getStorage(keys) {
return new Promise((resolve, reject) => {
chrome.storage.sync.get(keys, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve(result);
}
});
});
}
static async removeStorage(keys) {
return new Promise((resolve, reject) => {
chrome.storage.sync.remove(keys, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve(result);
}
});
});
}
}
export class AsyncLocalStorage {
static async setStorage(items) {
return new Promise((resolve, reject) => {
chrome.storage.local.set(items, () => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve();
}
});
});
}
static async getStorage(keys) {
return new Promise((resolve, reject) => {
chrome.storage.local.get(keys, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve(result);
}
});
});
}
static async removeStorage(keys) {
return new Promise((resolve, reject) => {
chrome.storage.local.remove(keys, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError));
} else {
resolve(result);
}
});
});
}
}
export class DeviceManager {
static async saveWidevineDevice(name, value) {
const result = await AsyncSyncStorage.getStorage(['devices']);
const array = result.devices === undefined ? [] : result.devices;
array.push(name);
await AsyncSyncStorage.setStorage({ devices: array });
await AsyncSyncStorage.setStorage({ [name]: value });
}
static async loadWidevineDevice(name) {
const result = await AsyncSyncStorage.getStorage([name]);
return result[name] || "";
}
static setWidevineDevice(name, value){
const wvd_combobox = document.getElementById('wvd-combobox');
const wvd_element = document.createElement('option');
wvd_element.text = name;
wvd_element.value = value;
wvd_combobox.appendChild(wvd_element);
}
static async loadSetAllWidevineDevices() {
const result = await AsyncSyncStorage.getStorage(['devices']);
const array = result.devices || [];
for (const item of array) {
this.setWidevineDevice(item, await this.loadWidevineDevice(item));
}
}
static async saveSelectedWidevineDevice(name) {
await AsyncSyncStorage.setStorage({ selected: name });
}
static async getSelectedWidevineDevice() {
const result = await AsyncSyncStorage.getStorage(["selected"]);
return result["selected"] || "";
}
static async selectWidevineDevice(name) {
document.getElementById('wvd-combobox').value = await this.loadWidevineDevice(name);
}
static async removeSelectedWidevineDevice() {
const selected_device_name = await DeviceManager.getSelectedWidevineDevice();
const result = await AsyncSyncStorage.getStorage(['devices']);
const array = result.devices === undefined ? [] : result.devices;
const index = array.indexOf(selected_device_name);
if (index > -1) {
array.splice(index, 1);
}
await AsyncSyncStorage.setStorage({ devices: array });
await AsyncSyncStorage.removeStorage([selected_device_name]);
}
}
export class SettingsManager {
static async setEnabled(enabled) {
await AsyncSyncStorage.setStorage({ enabled: enabled });
}
static async getEnabled() {
const result = await AsyncSyncStorage.getStorage(["enabled"]);
return result["enabled"] === undefined ? false : result["enabled"];
}
static downloadFile(content, filename) {
const blob = new Blob([content], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
static async importDevice(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = async function (loaded) {
const result = loaded.target.result;
const widevine_device = new WidevineDevice(result);
const b64_device = uint8ArrayToBase64(new Uint8Array(result));
const device_name = widevine_device.get_name();
if (!await DeviceManager.loadWidevineDevice(device_name)) {
await DeviceManager.saveWidevineDevice(device_name, b64_device);
}
await DeviceManager.saveSelectedWidevineDevice(device_name);
resolve();
};
reader.readAsArrayBuffer(file);
})
}
}
export function compareUint8Arrays(arr1, arr2) {
if (arr1.length !== arr2.length)
return false;
return Array.from(arr1).every((value, index) => value === arr2[index]);
}
export function uint8ArrayToHex(buffer) {
return Array.prototype.map.call(buffer, x => x.toString(16).padStart(2, '0')).join('');
}
export function uint8ArrayToString(uint8array) {
return String.fromCharCode.apply(null, uint8array)
}
export function uint8ArrayToBase64(uint8array) {
return btoa(String.fromCharCode.apply(null, uint8array));
}
export function base64toUint8Array(base64_string){
return Uint8Array.from(atob(base64_string), c => c.charCodeAt(0))
}
export function stringToUint8Array(string) {
return Uint8Array.from(string.split("").map(x => x.charCodeAt()))
}
export function stringToHex(string){
return string.split("").map(c => c.charCodeAt(0).toString(16).padStart(2, "0")).join("");
}