commit 0e04cb1285bb911078ea193c5ad917802037f2e3 Author: CDM-Project Date: Sun Sep 8 20:13:39 2024 -0400 Reupload diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..870e68e --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea +/databases/key_cache.db +/databases/WVDs/* +/static/css/tailwind.css +/package.json +/package-lock.json +/tailwind.config.js +/databases/users.db +*.pyc +/databases/devine.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..900f235 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +## CDRM-Project + ![forthebadge](https://forthebadge.com/images/badges/uses-html.svg) ![forthebadge](https://forthebadge.com/images/badges/uses-css.svg) ![forthebadge](https://forthebadge.com/images/badges/uses-javascript.svg) ![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg) + ## What is this? + + An open source web application written in python to decrypt Widevine protected content. + +## Prerequisites + + - [Python](https://www.python.org/downloads/) with PIP installed + + > Python 3.12 was used at the time of writing + + ## Installation + + - Open your terminal and navigate to where you'd like to store the application + - Create a new python virtual environment using `python -m venv CDRM-Project` + - Change directory into the new `CDRM-Project` folder + - Activate the virtual environment + + > Windows - change directory into the `Scripts` directory then `activate.bat` + > + > Linux - `source bin/activate` + + - Extract CDRM-Project 2.0 git contents into the newly created `CDRM-Project` folder + - Install python dependencies `pip install -r requirements.txt` + - (Optional) Place your .WVD file into `/databases/WVDs` + - Run the application `python main.py` + diff --git a/main.py b/main.py new file mode 100644 index 0000000..dbb6d3e --- /dev/null +++ b/main.py @@ -0,0 +1,420 @@ +# Import dependencies +from flask import Flask, render_template, request, jsonify, session, redirect, send_file, Response, current_app +import requests +import scripts +import uuid +import json +import base64 +from pywidevine import __version__ +from pywidevine.pssh import PSSH +from pywidevine.cdm import Cdm +from pywidevine.device import Device +from pywidevine import RemoteCdm + +# Create database if it doesn't exist +scripts.create_database.create_database() + +# Check for .WVD file and assign it a variable +WVD = scripts.wvd_check.check_for_wvd() + +# If no WVD found, exit +if WVD is None: + WVD = 'Remote' + rcdm = RemoteCdm( + device_type='ANDROID', + system_id=int(requests.post(url='https://cdrm-project.com/devine').content), + security_level=3, + host='https://cdrm-project.com/devine', + secret='CDRM-Project', + device_name='CDM' + ) + +# Define Flask app object, give template and static arguments. +app = Flask(__name__, template_folder='templates', static_folder='static', static_url_path='/') + +# Create a secret key for logins +app.secret_key = str(uuid.uuid4()) + +# Route for root '/' +@app.route("/", methods=['GET', 'POST']) +def main_page(): + if request.method == 'GET': + return render_template('index.html') + elif request.method == 'POST': + + # Get the JSON data from the request + data = json.loads(request.data.decode()) + + # Get the proxy + proxy = data['Proxy'] + if proxy == '': + proxy = None + + decrypt_response = scripts.decrypt.decrypt_content( + in_pssh=request.json['PSSH'], + license_url=request.json['License URL'], + headers=request.json['Headers'], + json_data=request.json['JSON'], + cookies_data=request.json['Cookies'], + input_data=request.json['Data'], + wvd=WVD, + proxy=proxy, + ) + return jsonify(decrypt_response) + + +# Route for '/cache' +@app.route("/cache", methods=['GET', 'POST']) +def cache_page(): + if request.method == 'GET': + cache_page_key_count = scripts.key_count.count_keys() + return render_template('cache.html', cache_page_key_count=cache_page_key_count) + if request.method == 'POST': + results = scripts.vault_check.check_database(pssh=request.json['PSSH']) + message = { + 'Message': results + } + return jsonify(message) + +@app.route("/key_count", methods=['GET']) +def key_count(): + if request.method == 'GET': + results = scripts.key_count.count_keys() + results = 'Total Keys: ' + str(results) + message = { + 'Message': results + } + return jsonify(message) + +# Route for '/faq' +@app.route("/faq", methods=['GET']) +def faq_page(): + if request.method == 'GET': + return render_template('faq.html') + + +@app.route("/api", methods=['GET', 'POST']) +def api_page(): + if request.method == 'GET': + return render_template('api.html') + elif request.method == 'POST': + return + + +@app.route("/extension", methods=['GET', 'POST']) +def extension_page(): + if request.method == 'GET': + return render_template('extension.html') + + elif request.method == 'POST': + # Get the JSON data from the request + data = json.loads(request.data.decode()) + + # Get the MPD url + json_data = data['JSON'] + if json_data: + try: + json_data = base64.b64decode(json_data).decode() + json_data = json.loads(json_data) + except: + json_data = json_data + + # Get the proxy + proxy = data['Proxy'] + if proxy == '': + proxy = None + + try: + keys = scripts.extension_decrypt.decrypt_content(in_pssh=data['PSSH'], license_url=data['License URL'], headers=json.loads(data['Headers']), + wvd=WVD, scheme=data['Scheme'], proxy=proxy, json_data=json_data) + return {'Message': f'{keys}'} + except Exception as error: + return {"Message": [f'{error}']} + + +@app.route("/download-extension", methods=['GET', 'POST']) +def download_extension_page(): + if request.method == 'GET': + file_path = 'static/assets/wvg-next-cdrm.zip' + return send_file(file_path, as_attachment=True) + elif request.method == 'POST': + version = { + 'Version': '1.16' + } + return jsonify(version) + + +# Route for '/login' +@app.route("/login", methods=['GET', 'POST']) +def login_page(): + if request.method == 'GET': + if session.get('logged_in'): + return redirect('/profile') + else: + return render_template('login.html') + if request.method == 'POST': + username = request.json['Username'] + if scripts.check_user.check_username_exist(username=username.lower()): + if scripts.check_user.check_password(username=username.lower(), password=request.json['Password']): + session['logged_in'] = True + return { + 'Message': 'Success' + } + else: + return { + 'Message': 'Failed to Login' + } + else: + return { + 'Message': 'Username does not exist' + } + + +# Route for '/register' +@app.route("/register", methods=['POST']) +def register(): + if request.method == 'POST': + username = request.json['Username'] + if username == '': + return { + 'Message': 'Username cannot be empty' + } + if not scripts.check_user.check_username_exist(username=username.lower()): + scripts.check_user.insert_user(username=username.lower(), password=request.json['Password']) + session['logged_in'] = True + return { + 'Message': 'Success' + } + else: + return { + 'Message': 'Username already taken' + } + + +# Route for '/profile' +@app.route("/profile", methods=['GET']) +def profile(): + if request.method == 'GET': + return render_template('profile.html') + + +# Route for '/devine' +@app.route("/devine", methods=['GET', 'POST', 'HEAD']) +def devine_page(): + if request.method == 'GET': + if WVD != 'Remote': + cdm = Cdm.from_device(device=Device.load(WVD)) + cdm_version = cdm.system_id + else: + cdm = rcdm + cdm_version = cdm.system_id + devine_service_count = scripts.key_count.get_service_count_devine() + devine_key_count = scripts.key_count.count_keys_devine() + return render_template('devine.html', cdm_version=cdm_version, devine_service_count=devine_service_count, devine_key_count=devine_key_count) + if request.method == 'POST': + if WVD != 'Remote': + cdm = Cdm.from_device(device=Device.load(WVD)) + cdm_version = cdm.system_id + else: + cdm = rcdm + cdm_version = cdm.system_id + return str(cdm_version) + if request.method == 'HEAD': + response = Response(status=200) + response.headers.update({ + "Server": f"https://github.com/devine-dl/pywidevine serve v{__version__}" + }) + return response + + +# Route for '/{device}/open' +@app.route("/devine//open", methods=['GET']) +def device_open(device): + if request.method == 'GET': + if WVD != 'Remote': + cdm_device = Device.load(WVD) + cdm = current_app.config['cdms'] = Cdm.from_device(cdm_device) + else: + cdm = current_app.config['cdms'] = rcdm + session_id = cdm.open() + response_data = { + "status": 200, + "message": "Success", + "data": { + "session_id": session_id.hex(), + "device": { + "system_id": cdm.system_id, + "security_level": cdm.security_level + } + } + } + return jsonify(response_data) + + +# Route for '/{device}/set_service_certificate' +@app.route("/devine//set_service_certificate", methods=['POST']) +def set_cert(device): + if request.method == 'POST': + cdm = current_app.config["cdms"] + body = request.json + + # get session id + session_id = bytes.fromhex(body["session_id"]) + + # set service certificate + certificate = body.get("certificate") + + provider_id = cdm.set_service_certificate(session_id, certificate) + + response_data = { + "status": 200, + "message": f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.", + "data": { + "provider_id": provider_id + } + } + + return jsonify(response_data) + + +@app.route("/devine//get_license_challenge/", methods=['POST']) +def get_license_challenge_page(device, licensetype): + if request.method == 'POST': + cdm = current_app.config["cdms"] + + body = request.json + + session_id = bytes.fromhex(body["session_id"]) + + privacy_mode = body.get("privacy_mode", True) + + current_app.config['PSSH'] = body['init_data'] + init_data = PSSH(body["init_data"]) + + license_request = cdm.get_license_challenge( + session_id=session_id, + pssh=init_data, + license_type=licensetype, + privacy_mode=privacy_mode + ) + + results = { + "status": 200, + "message": "Success", + "data": { + "challenge_b64": base64.b64encode(license_request).decode() + } + } + + return jsonify(results) + + +@app.route("/devine//parse_license", methods=['POST']) +def parse_license(device): + if request.method == 'POST': + cdm = current_app.config["cdms"] + + body = request.json + + session_id = bytes.fromhex(body["session_id"]) + + cdm.parse_license(session_id, body["license_message"]) + + results = { + "status": 200, + "message": "Successfully parsed and loaded the Keys from the License message." + } + + return jsonify(results) + + +@app.route("/devine//get_keys/", methods=['POST']) +def get_keys(device, key_type): + if request.method == 'POST': + cdm = current_app.config["cdms"] + + body = request.json + + session_id = bytes.fromhex(body["session_id"]) + + if key_type == "ALL": + key_type = None + + keys = cdm.get_keys(session_id, key_type) + returned_keys = "" + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + returned_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + keys_json = [ + { + "key_id": key.kid.hex, + "key": key.key.hex(), + "type": key.type, + "permissions": key.permissions, + } + for key in keys + if not key_type or key.type == key_type + ] + + results = { + "status": 200, + "message": "Success", + "data": { + "keys": keys_json + } + } + + scripts.key_cache.cache_keys(pssh=current_app.config['PSSH'], keys=returned_keys) + + return jsonify(results) + + +@app.route("/devine//close/", methods=['GET']) +def close_session(device, session_id): + if request.method == 'GET': + cdm = current_app.config["cdms"] + + session_id = bytes.fromhex(session_id) + + cdm.close(session_id) + + results = { + "status": 200, + "message": f"Successfully closed Session '{session_id.hex()}'." + } + + return jsonify(results) + +@app.route("/devine/vault/", methods=['POST']) +def add_keys_devine_vault(service): + data = request.json['content_keys'] + replaced = 0 + inserted = 0 + for key_id, key in data.items(): + result = scripts.key_cache.cache_keys_devine(service=service, kid=key_id, key=key) + if result == 'inserted': + inserted += 1 + if result == 'replaced': + replaced += 1 + + message = { + "code": 0, + "added": inserted, + "updated": replaced + } + return jsonify(message) + +@app.route("/devine/vault//", methods=['GET']) +def get_key_devine_vault(service, kid): + key = scripts.vault_check.get_key_by_kid_and_service(service=service, kid=kid) + message = { + "code": 0, + "content_key": key + } + return jsonify(message) + + +# If the script is called directly, start the flask app. +if __name__ == '__main__': + app.run(debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ced4633 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Flask +pywidevine +requests +tls-client +typing-extensions \ No newline at end of file diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..0338678 --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1,9 @@ +from . import create_database +from . import wvd_check +from . import decrypt +from . import vault_check +from . import key_cache +from . import check_user +from . import gen_hash +from . import key_count +from . import extension_decrypt \ No newline at end of file diff --git a/scripts/check_user.py b/scripts/check_user.py new file mode 100644 index 0000000..5d8af93 --- /dev/null +++ b/scripts/check_user.py @@ -0,0 +1,65 @@ +import sqlite3 +import os +from . import gen_hash + + +def check_username_exist(username): + # Connect to the SQLite database + conn = sqlite3.connect(f'{os.getcwd()}/databases/users.db') + cursor = conn.cursor() + + # Execute a SELECT query to check if the username exists + cursor.execute("SELECT * FROM DATABASE WHERE username=?", (username,)) + + # Fetch the result + result = cursor.fetchone() + + # Close the database connection + conn.close() + + # If result is not None, username exists in the database, return True + if result: + return True + else: + return False + + +def check_password(username, password): + # Connect to the SQLite database + conn = sqlite3.connect(f'{os.getcwd()}/databases/users.db') + cursor = conn.cursor() + + # Execute a SELECT query to retrieve the password for the given username + cursor.execute("SELECT hashedpassword FROM DATABASE WHERE username=?", (username,)) + + # Fetch the result + result = cursor.fetchone() + + # Close the database connection + conn.close() + + # If result is not None, username exists in the database, return the password + if result[0]: + if result[0] == gen_hash.generate_md5(username.lower(), password): + return True + else: + return False + + +def insert_user(username, password): + # Connect to the SQLite database + conn = sqlite3.connect(f'{os.getcwd()}/databases/users.db') + cursor = conn.cursor() + + try: + # Execute an INSERT query to insert the username and password into the users table + cursor.execute("INSERT INTO DATABASE (username, hashedpassword) VALUES (?, ?)", (username.lower(), gen_hash.generate_md5(username.lower(), password))) + # Commit the transaction + conn.commit() + except sqlite3.Error as e: + print(e) + # Rollback the transaction in case of any error + conn.rollback() + finally: + # Close the database connection + conn.close() diff --git a/scripts/create_database.py b/scripts/create_database.py new file mode 100644 index 0000000..3a6bafe --- /dev/null +++ b/scripts/create_database.py @@ -0,0 +1,54 @@ +# import dependencies +import sqlite3 +import os + + +# Check to see if the database already exists, if not create a databases folder, and create the database. +def create_database(): + + # Check to see if the "databases" directory exists, if not creates it + if "databases" not in os.listdir(): + os.makedirs('databases') + + # Change to the databases directory + os.chdir("databases") + + # Check to see if a database exists in databases directory, if not create it + if not os.path.isfile("key_cache.db"): + + # Connect / Create "key_cache.db" + dbconnection = sqlite3.connect("key_cache.db") + + # Create cursor + dbcursor = dbconnection.cursor() + + # Create table through the cursor + dbcursor.execute('CREATE TABLE IF NOT EXISTS "DATABASE" ( "pssh" TEXT, "keys" TEXT, PRIMARY KEY("pssh") )') + + # Close the connection + dbconnection.close() + + # Check to see if a database exists in databases directory, if not create it + if not os.path.isfile("users.db"): + # Connect / Create "key_cache.db" + dbconnection = sqlite3.connect("users.db") + + # Create cursor + dbcursor = dbconnection.cursor() + + # Create table through the cursor + dbcursor.execute('CREATE TABLE IF NOT EXISTS "DATABASE" ( "username" TEXT, "hashedpassword" TEXT, ' + '"cached_keys" TEXT, PRIMARY KEY("username") )') + + # Close the connection + dbconnection.close() + + # Check to see if a database exists in databases directory, if not create it + if not os.path.isfile("devine.db"): + dbconnection = sqlite3.connect("devine.db") + dbcursor = dbconnection.cursor() + dbcursor.execute('CREATE TABLE IF NOT EXISTS "vault" ( "service" TEXT, "kid" TEXT, ' + '"key" TEXT, PRIMARY KEY("kid") )') + + # Change back to root directory + os.chdir(os.path.join(os.getcwd(), "..")) \ No newline at end of file diff --git a/scripts/decrypt.py b/scripts/decrypt.py new file mode 100644 index 0000000..4802fe3 --- /dev/null +++ b/scripts/decrypt.py @@ -0,0 +1,220 @@ +# import dependencies +import base64 +import json + +from pywidevine import PSSH +from pywidevine import Cdm +from pywidevine import Device +from pywidevine import RemoteCdm +import requests +import ast +from . import key_cache + + +# Define a function to clean dictionary values sent as string from the post request +def clean_my_dict(dirty_dict: str = None): + header_string = f"'''" \ + f"{dirty_dict}" \ + f"'''" + cleaned_string = '\n'.join(line for line in header_string.split('\n') if not line.strip().startswith('#')) + clean_dict = ast.literal_eval(cleaned_string) + return clean_dict + + +# Defining decrypt function +def decrypt_content(in_pssh: str = None, license_url: str = None, + headers: str = None, json_data: str = None, cookies_data: str = None, input_data: str = None, wvd: str = None, proxy: str = None,): + # prepare pssh + try: + pssh = PSSH(in_pssh) + except Exception as error: + return { + 'Message': str(error) + } + + if wvd != 'Remote': + + # load device + device = Device.load(wvd) + + # load CDM from device + cdm = Cdm.from_device(device) + + else: + cdm = RemoteCdm( + device_type='ANDROID', + system_id=int(requests.post(url='https://cdrm-project.com/devine').content), + security_level=3, + host='https://cdrm-project.com/devine', + secret='CDRM-Project', + device_name='CDM' + ) + + # open CDM session + session_id = cdm.open() + + # Generate the challenge + challenge = cdm.get_license_challenge(session_id, pssh) + + challenge_not_in_data = False + extra_data_challenge = False + + if headers != '': + try: + headers = ast.literal_eval(clean_my_dict(dirty_dict=headers)) + + # Iterate through the dictionary + for key, value in headers.items(): + # Check if the value is '!Challenge' + if value == '!Challenge': + # Replace the value with something else + headers[key] = base64.b64encode(challenge).decode() + challenge_not_in_data = True + except: + return { + 'Message': 'Headers could not be loaded correctly, please make sure they are formatted in python dictionary' + } + + if json_data != '': + try: + json_data = ast.literal_eval(clean_my_dict(dirty_dict=json_data)) + # Iterate through the dictionary + for key, value in json_data.items(): + # Check if the value is '!Challenge' + if value == '!Challenge': + # Replace the value with something else + json_data[key] = base64.b64encode(challenge).decode() + challenge_not_in_data = True + except: + return { + 'Message': 'JSON could not be loaded correctly, please make sure they are formatted in python dictionary format' + } + + if cookies_data != '': + try: + cookies_data = ast.literal_eval(clean_my_dict(dirty_dict=cookies_data)) + # Iterate through the dictionary + for key, value in cookies_data.items(): + # Check if the value is '!Challenge' + if value == '!Challenge': + # Replace the value with something else + cookies_data[key] = base64.b64encode(challenge).decode() + challenge_not_in_data = True + except: + return { + 'Message': 'Cookies could not be loaded correctly, please make sure they are formatted in python dictionary format' + } + + if input_data != '': + try: + input_data = ast.literal_eval(clean_my_dict(dirty_dict=input_data)) + # Iterate through the dictionary + for key, value in input_data.items(): + # Check if the value is '!Challenge' + if value == '!Challenge': + # Replace the value with something else + input_data[key] = base64.b64encode(challenge).decode() + extra_data_challenge = True + except: + return { + 'Message': 'Data could not be loaded correctly, please make sure they are formatted in python dictionary format' + } + + # Try statement here, probably the most common point of failure + try: + if extra_data_challenge == False: + if challenge_not_in_data == False: + + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + json=json_data, + cookies=cookies_data, + data=challenge, + proxies={ + 'http': proxy, + } + ) + else: + license = requests.post( + url=license_url, + headers=headers, + json=json_data, + cookies=cookies_data, + proxies={ + 'http': proxy, + } + ) + else: + print("Extra challenge!!") + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + json=json_data, + cookies=cookies_data, + data=input_data, + proxies={ + 'http': proxy, + } + ) + if license.status_code != 200: + license = requests.post( + url=license_url, + headers=headers, + json=json_data, + cookies=cookies_data, + data=json.dumps(input_data), + proxies={ + 'http': proxy, + } + ) + + + except Exception as error: + return { + 'Message': f'An error occured {error}\n\n{license.content}' + } + + # Another try statement to parse licenses + try: + cdm.parse_license(session_id, license.content) + except: + try: + cdm.parse_license(session_id, license.json().get('license')) + except: + try: + replaced_license = license.json()["license"].replace("-", "+").replace("_", "/") + cdm.parse_license(session_id, replaced_license) + except: + try: + cdm.parse_license(session_id, license.json().get('licenseData')) + except: + try: + cdm.parse_license(session_id, license.json().get('widevine2License')) + except: + try: + cdm.parse_license(session_id, license.json().get('license')[0]) + except Exception as error: + return { + 'Message': f'An error occured {error}\n\n{license.content}' + } + + + # assign variable for returned keys + returned_keys = "" + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + returned_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # close session, disposes of session data + cdm.close(session_id) + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=returned_keys) + + # Return the keys + return { + 'Message': returned_keys + } diff --git a/scripts/extension_decrypt.py b/scripts/extension_decrypt.py new file mode 100644 index 0000000..16097f3 --- /dev/null +++ b/scripts/extension_decrypt.py @@ -0,0 +1,389 @@ +# import dependencies +from pywidevine import PSSH +from pywidevine import Cdm +from pywidevine import Device +from pywidevine import RemoteCdm +import requests +from . import key_cache +import base64 +import tls_client +from xml.etree import ElementTree as ET + +# Defining decrypt function +def decrypt_content(in_pssh: str = None, license_url: str = None, headers: dict = None, json_data: dict = None, wvd: str = None, scheme: str = None, proxy: str = None,): + + # prepare pssh + pssh = PSSH(in_pssh) + + if wvd != 'Remote': + + # load device + device = Device.load(wvd) + + # load CDM from device + cdm = Cdm.from_device(device) + + else: + cdm = RemoteCdm( + device_type='ANDROID', + system_id=int(requests.post(url='https://cdrm-project.com/devine').content), + security_level=3, + host='https://cdrm-project.com/devine', + secret='CDRM-Project', + device_name='CDM' + ) + + # open CDM session + session_id = cdm.open() + + # Generate the challenge + challenge = cdm.get_license_challenge(session_id, pssh) + + if scheme == 'CommonWV': + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + data=challenge, + proxies={ + 'http': proxy, + }, + ) + + # Parse the license if it comes back in plain bytes + try: + cdm.parse_license(session_id, license.content) + except: + # Exception, try to find by regex via json + try: + cdm.parse_license(session_id, license.json()['license']) + except: + try: + cdm.parse_license(session_id, license.json()['licenseData']) + except Exception as error: + return f'{error}\n\n{license.content}' + + # Assign variable for caching keys + cached_keys = "" + + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + cached_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=cached_keys) + + # close session, disposes of session data + cdm.close(session_id) + + # Return the keys + return cached_keys + + if scheme == 'Amazon': + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + data={ + 'widevine2Challenge': f'{base64.b64encode(challenge).decode()}', + 'includeHdcpTestKeyInLicense': 'true', + }, + proxies={ + 'http': proxy, + } + ) + + # Parse the license if it comes back in plain bytes + try: + cdm.parse_license(session_id, license.json()['widevine2License']['license']) + except Exception as error: + return [f'{error}\n\n{license.content}'] + + # Assign variable for caching keys + cached_keys = "" + + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + cached_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=cached_keys) + + # close session, disposes of session data + cdm.close(session_id) + + # Return the keys + return cached_keys + + if scheme == 'YouTube': + json_data['licenseRequest'] = base64.b64encode(challenge).decode() + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + json=json_data, + proxies={ + 'http': proxy, + } + ) + # Extract license from json dict + license = license.json()["license"].replace("-", "+").replace("_", "/") + + # Parse the license if it comes back in plain bytes + try: + cdm.parse_license(session_id, license) + except Exception as error: + return f'{error}\n\n{license.content}' + + # Assign variable for caching keys + cached_keys = "" + + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + cached_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=cached_keys) + + # close session, disposes of session data + cdm.close(session_id) + + # Return the keys + return cached_keys + + if scheme == 'RTE': + json_data['getWidevineLicense']['widevineChallenge'] = base64.b64encode(challenge).decode() + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + json=json_data, + proxies={ + 'http': proxy, + } + ) + + # Parse the license if it comes back in plain bytes + try: + cdm.parse_license(session_id, license.json()['getWidevineLicenseResponse']['license']) + except Exception as error: + return f'{error}\n\n{license.content}' + + # Assign variable for caching keys + cached_keys = "" + + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + cached_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=cached_keys) + + # close session, disposes of session data + cdm.close(session_id) + + # Return the keys + return cached_keys + + if scheme == 'Canal+': + + try: + json_data['ServiceRequest']['InData']['ChallengeInfo'] = base64.b64encode(challenge).decode() + + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + json=json_data + ) + + # Parse the license + try: + cdm.parse_license(session_id, license.json()['ServiceResponse']['OutData']['LicenseInfo']) + except Exception as error: + return f'{error}\n\n{license.content}' + + # Assign variable for caching keys + cached_keys = "" + + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + cached_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=cached_keys) + + # close session, disposes of session data + cdm.close(session_id) + + # Return the keys + return cached_keys + + except: + + try: + session = tls_client.Session() + + response = session.post( + url=license_url, + headers=headers, + data=f'{base64.b64encode(challenge).decode()}' + ).content.decode() + + # Define the namespace + namespace = {'ns': 'http://www.canal-plus.com/DRM/V1'} + + # Parse the XML string + root = ET.fromstring(response) + + # Find the license element using the namespace + license_element = root.find('.//ns:license', namespace) + + # Extract the text content of the license element + license_content = license_element.text + + # Parse the license + try: + cdm.parse_license(session_id, license_content) + except Exception as error: + return f'{error}\n\n{license.content}' + + # Assign variable for caching keys + cached_keys = "" + + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + cached_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=cached_keys) + + # close session, disposes of session data + cdm.close(session_id) + + # Return the keys + return cached_keys + + except Exception as error: + return [f'{error}\n\n{license.content}'] + + if scheme == 'NosTV': + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + json={ + 'challenge': base64.b64encode(challenge).decode(), + }, + proxies={ + 'http': proxy, + }, + ) + + # Parse the license if it comes back in plain bytes + try: + cdm.parse_license(session_id, license.json()['license'][0]) + except Exception as error: + return f'{error}\n\n{license.content}' + + # Assign variable for caching keys + cached_keys = "" + + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + cached_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=cached_keys) + + # close session, disposes of session data + cdm.close(session_id) + + # Return the keys + return cached_keys + + if scheme == 'AstroGo': + + # Insert the challenge + json_data['licenseChallenge'] = base64.b64encode(challenge).decode() + + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + json=json_data, + proxies={ + 'http': proxy, + }, + ) + + # Parse the license if it comes back in plain bytes + try: + cdm.parse_license(session_id, license.content) + except: + try: + cdm.parse_license(session_id, license.json()['licenseData']) + except: + try: + cdm.parse_license(session_id, license.json()['licenseData'][0]) + except: + try: + cdm.parse_license(session_id, str(license.json()['licenseData'][0])) + except Exception as error: + return f'{error}\n\n{license.content}' + + # Assign variable for caching keys + cached_keys = "" + + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + cached_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=cached_keys) + + # close session, disposes of session data + cdm.close(session_id) + + # Return the keys + return cached_keys + + if scheme == 'Vodafone': + print(json_data) + + # Find the challenge field + for request in json_data['requests']: + if request['action'] == 'license': + request['params']['challenge'] = base64.b64encode(challenge).decode() + + # send license challenge + license = requests.post( + url=license_url, + headers=headers, + json=json_data, + proxies={ + 'http': proxy, + }, + ) + + # Parse the license if it comes back in plain bytes + try: + cdm.parse_license(session_id, license.json()['license']) + except Exception as error: + return f'{error}\n\n{license.content}' + + # Assign variable for caching keys + cached_keys = "" + + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + cached_keys += f"{key.kid.hex}:{key.key.hex()}\n" + + # Cache the keys + key_cache.cache_keys(pssh=in_pssh, keys=cached_keys) + + # close session, disposes of session data + cdm.close(session_id) + + # Return the keys + return cached_keys \ No newline at end of file diff --git a/scripts/gen_hash.py b/scripts/gen_hash.py new file mode 100644 index 0000000..e73aee0 --- /dev/null +++ b/scripts/gen_hash.py @@ -0,0 +1,11 @@ +import hashlib + +def generate_md5(user_login, user_pass): + # Concatenate username and password + data = user_login.encode() + user_pass.encode() + + # Create MD5 hash + user_md5_hash = hashlib.md5(data).hexdigest() + + return user_md5_hash + diff --git a/scripts/key_cache.py b/scripts/key_cache.py new file mode 100644 index 0000000..69f5528 --- /dev/null +++ b/scripts/key_cache.py @@ -0,0 +1,50 @@ +# Import dependencies +import sqlite3 +import os + + +# Define cache function +def cache_keys(pssh: str, keys: str): + + # Connect to database + dbconnection = sqlite3.connect(f"{os.getcwd()}/databases/key_cache.db") + + # Initialize a cursor + dbcursor = dbconnection.cursor() + + # Insert PSSH and keys + dbcursor.execute("INSERT or REPLACE INTO database VALUES (?, ?)", (pssh, keys)) + + # Commit the changes + dbconnection.commit() + + # Close the connection + dbconnection.close() + +def cache_keys_devine(service: str, kid: str, key: str): + + # Connect to database + dbconnection = sqlite3.connect(f"{os.getcwd()}/databases/devine.db") + + # Initialize a cursor + dbcursor = dbconnection.cursor() + + # Check if the key already exists + dbcursor.execute("SELECT COUNT(*) FROM vault WHERE service = ? AND kid = ?", (service, kid)) + row = dbcursor.fetchone() + if row[0] > 0: + # Key already exists, perform REPLACE operation + dbcursor.execute("REPLACE INTO vault VALUES (?, ?, ?)", (service, kid, key)) + operation = "replaced" + else: + # Key does not exist, perform INSERT operation + dbcursor.execute("INSERT INTO vault VALUES (?, ?, ?)", (service, kid, key)) + operation = "inserted" + + # Commit the changes + dbconnection.commit() + + # Close the connection + dbconnection.close() + + return operation diff --git a/scripts/key_count.py b/scripts/key_count.py new file mode 100644 index 0000000..c8de2b5 --- /dev/null +++ b/scripts/key_count.py @@ -0,0 +1,67 @@ +# Import dependencies +import sqlite3 +import os + + +# Define cache function +def count_keys(): + + # Connect to database + dbconnection = sqlite3.connect(f"{os.getcwd()}/databases/key_cache.db") + + # Initialize a cursor + dbcursor = dbconnection.cursor() + + # Get the count + dbcursor.execute("SELECT COUNT (*) FROM database") + + # Get the result + result = list(dbcursor) + + # Convert it into a string + formatted_result = str(result) + + # Strip special characters + stripped_result = formatted_result.strip("[](),") + + # return the result + return stripped_result + + +def count_keys_devine(): + + # Connect to the database + connection = sqlite3.connect(f"{os.getcwd()}/databases/devine.db") + cursor = connection.cursor() + + # Execute the query to count total number of keys + cursor.execute("SELECT COUNT(kid) FROM vault") + + # Fetch the result + total_keys = cursor.fetchone()[0] + + # Close the cursor and connection + cursor.close() + connection.close() + + # return the key count + return total_keys + + +def get_service_count_devine(): + # Connect to the database + connection = sqlite3.connect(f"{os.getcwd()}/databases/devine.db") + cursor = connection.cursor() + + # Execute the query to count unique services + cursor.execute("SELECT COUNT(DISTINCT service) FROM vault") + + # Fetch the result + count = cursor.fetchone()[0] + + # Close the cursor and connection + cursor.close() + connection.close() + + # return the count + return count \ No newline at end of file diff --git a/scripts/vault_check.py b/scripts/vault_check.py new file mode 100644 index 0000000..6e1c907 --- /dev/null +++ b/scripts/vault_check.py @@ -0,0 +1,68 @@ +# Import dependencies +import sqlite3 +import os + + +# Define check database function +def check_database(pssh: str): + + # Connect to DB + dbconnection = sqlite3.connect(f"{os.getcwd()}/databases/key_cache.db") + + # Initialize a cursor object + dbcursor = dbconnection.cursor() + + # Find PSSH + dbcursor.execute("SELECT keys FROM database WHERE pssh = :pssh", {"pssh": pssh}) + + # Fetch all results + vaultkeys = dbcursor.fetchall() + + # If any found + if vaultkeys: + + # Assign variable + vaultkey = str(vaultkeys[0]) + + # Strip of sqlite special characters + stripped_vault_key = vaultkey.strip(",'()") + + # Remove double \\ for single \ + formatted_vault_key = stripped_vault_key.replace('\\n', '\n') + + # Close the connections + dbconnection.close() + + return formatted_vault_key + + # If no keys found + else: + + # Close the connection + dbconnection.close() + + # Return not found + return "Not found" + + +def get_key_by_kid_and_service(service: str, kid: str): + # Connect to the database + dbconnection = sqlite3.connect(f"{os.getcwd()}/databases/devine.db") + dbcursor = dbconnection.cursor() + + # Execute the SELECT query + dbcursor.execute("SELECT key FROM vault WHERE service = ? AND kid = ?", (service, kid)) + + # Fetch the result + result = dbcursor.fetchone() + + # Close the connection + dbconnection.close() + + # If result is None, no matching key found + if result is None: + return None + else: + return result[0] # Returning the key + + diff --git a/scripts/wvd_check.py b/scripts/wvd_check.py new file mode 100644 index 0000000..314c4e8 --- /dev/null +++ b/scripts/wvd_check.py @@ -0,0 +1,22 @@ +# Import dependencies +import os +import glob + + +# Define WVD device check +def check_for_wvd(): + try: + + # Use glob to get the name of the .wvd + extracted_device = glob.glob(f'{os.getcwd()}/databases/WVDs/*.wvd')[0] + + # Return the device path + return extracted_device + except: + + # Check to see if the WVDs folder exist, if not create it + if 'WVDs' not in os.listdir(fr'{os.getcwd()}/databases'): + os.makedirs(f'{os.getcwd()}/databases/WVDs') + + # Stop the program and print out instructions + return None diff --git a/static/assets/background.png b/static/assets/background.png new file mode 100644 index 0000000..141aa0d Binary files /dev/null and b/static/assets/background.png differ diff --git a/static/assets/wvg-next-cdrm.zip b/static/assets/wvg-next-cdrm.zip new file mode 100644 index 0000000..f77104a Binary files /dev/null and b/static/assets/wvg-next-cdrm.zip differ diff --git a/static/css/api.css b/static/css/api.css new file mode 100644 index 0000000..426bf3c --- /dev/null +++ b/static/css/api.css @@ -0,0 +1,33 @@ +main { + display: grid; + grid-template-rows: auto auto 2fr; + margin-top: 5%; +} + +h1 { + color: white; + justify-self: center; +} + +h2 { + color: white; + justify-self: center; + border-bottom-style: solid; + border-bottom-width: thin; +} + +div { + display: grid; +} + +pre { + color: white; + justify-self: center; + border-style: solid; + text-align: left; + border-radius: 2%; + border-color: rgba(0, 0, 0, 0.3); + background-color: rgba(0, 0, 0, 0.1); + padding: 1%; + width: auto; +} \ No newline at end of file diff --git a/static/css/cache.css b/static/css/cache.css new file mode 100644 index 0000000..183c48e --- /dev/null +++ b/static/css/cache.css @@ -0,0 +1,60 @@ +main { + display: grid; + grid-template-rows: auto 2fr 1fr; + margin-top: 5%; +} + +h1 { + display: grid; + justify-self: center; + color: white; +} + +form { + display: grid; + align-self: center; + justify-self: center; + color: white; + border-style: solid; + border-color: rgba(0, 0, 0, 0.3); + border-radius: 2%; + border-width: thin; + background-color: rgba(0, 0, 0, 0.1); + padding: 5%; + width: min(25%); +} + +#total_keys { + justify-self: center; +} + +#cache_pssh_label { + margin-bottom: 2%; +} + +#cache_pssh { + margin-bottom: 2%; + background-color: rgba(0, 0, 0, 0.1); + color: white; +} + +#cache_submit { + width: 25%; + justify-self: center; +} + +#cache_results_container { + display: none; + align-self: center; + justify-self: center; + justify-content: center; + color: white; + border-style: solid; + border-color: rgba(0, 0, 0, 0.3); + border-radius: 2%; + border-width: thin; + background-color: rgba(0, 0, 0, 0.1); + width: 30%; + margin-bottom: 3%; +} + diff --git a/static/css/devine.css b/static/css/devine.css new file mode 100644 index 0000000..8594a86 --- /dev/null +++ b/static/css/devine.css @@ -0,0 +1,55 @@ +main { + display: grid; + grid-template-rows: 1fr auto 2fr; +} + +h1 { + margin-top: 5%; + align-self: center; + justify-self: center; + color: white; +} + +p { + color: white; + align-self: center; + justify-self: center; + text-align: center; + border-style: solid; + border-width: thin; + border-radius: 2%; + background-color: rgba(0, 0, 0, 0.1); + border-color: rgba(0, 0, 0, 0.3); + padding: 2%; +} + +#remote_cdm { + text-align: left; + justify-self: right; + margin-right: 2%; + align-self: stretch; +} + +#remote_vault { + text-align: left; + justify-self: left; + align-self: stretch; + margin-left: 2%; +} + +#vault_results { + grid-column: 1 / span 2; /* Span both columns */ + text-align: left; + width: 10%; + align-self: start; +} + +.dlinks { + color: grey; +} + +div { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; +} \ No newline at end of file diff --git a/static/css/extension.css b/static/css/extension.css new file mode 100644 index 0000000..8c9f520 --- /dev/null +++ b/static/css/extension.css @@ -0,0 +1,50 @@ +main { + display: grid; + grid-template-rows: auto auto 2fr; + margin-top: 5%; +} + +h1 { + color: white; + justify-self: center; +} + +h2 { + color: white; + justify-self: center; + border-bottom-style: solid; + border-bottom-width: thin; +} + +div { + display: grid; +} + +pre { + color: white; + justify-self: center; + border-style: solid; + text-align: left; + border-radius: 2%; + border-color: rgba(0, 0, 0, 0.3); + background-color: rgba(0, 0, 0, 0.1); + padding: 1%; + width: auto; +} + +#ex_a { + color: black; +} + +#ex_button { + border-style: solid; + border-width: thin; + border-radius: 2%; + background-color: rgba(255, 255, 255, 0.3); + border-color: rgba(0, 0, 0, 0.3); + +} + +.fox { + color: gray; +} \ No newline at end of file diff --git a/static/css/faq.css b/static/css/faq.css new file mode 100644 index 0000000..a674e24 --- /dev/null +++ b/static/css/faq.css @@ -0,0 +1,26 @@ +main { + display: grid; + grid-template-rows: auto 2fr; + justify-content: center; + width: 100%; + margin-top: 5%; +} + +h1 { + display: grid; + justify-self: center; + color: white; +} + +.details_box { + display: grid; + color: white; + border-style: solid; + border-width: thin; + border-color: rgba(0, 0, 0, 0.3); + padding: 10%; + background-color: rgba(0, 0, 0, 0.1); + height: auto; + margin-bottom: 5%; + border-radius: 2%; +} \ No newline at end of file diff --git a/static/css/index.css b/static/css/index.css new file mode 100644 index 0000000..d52b97e --- /dev/null +++ b/static/css/index.css @@ -0,0 +1,75 @@ +main { + display: grid; + grid-template-rows: auto auto auto; + margin-top: 5%; + margin-bottom: 3%; +} + +h1 { + display: grid; + grid-auto-rows: auto; + justify-self: center; + color: white; +} + +#decrypt_form { + display: grid; + grid-template-rows: auto auto auto auto auto auto; + width: 50%; + justify-self: center; + border-style: solid; + padding: 1%; + border-radius: 2%; + border-color: rgba(0, 0, 0, 0.3); + background-color: rgba(0, 0, 0, 0.1); + margin-bottom: 3%; + color: white; +} + +#index_pssh, +#license_url, +#headers, +#json, +#cookies, +#data, +#proxy{ + resize: none; /* Prevent textarea from being resizable */ + overflow: auto; /* Enable scrolling if content overflows */ + background-color: rgba(0, 0, 0, 0.1); + color: white; + margin: 1%; +} + +#decrypt_button { + width: 25%; + justify-self: center; + margin-top: 2%; +} + +#results { + display: none; + border-style: solid; + border-radius: 2%; + border-color: rgba(0, 0, 0, 0.3); + background-color: rgba(0, 0, 0, 0.1); + width: 50%; + justify-self: center; + margin-bottom: 3%; +} + +#keys_paragraph { + display: grid; + grid-template-rows: auto; + justify-self: center; + font-size: xx-large; + color: white; +} + +#decrypt_results { + display: grid; + grid-template-rows: auto; + justify-self: center; + text-align: center; + color: white; + width: 100%; +} \ No newline at end of file diff --git a/static/css/login.css b/static/css/login.css new file mode 100644 index 0000000..7bc3e8b --- /dev/null +++ b/static/css/login.css @@ -0,0 +1,40 @@ +main { + display: grid; +} + +#login_form { + display: grid; + align-content: center; + align-self: center; + justify-self: center; + grid-template-rows: auto auto auto auto auto auto; + width: 25%; + color: white; + border-style: solid; + border-width: thin; + border-radius: 2%; + border-color: rgba(0, 0, 0, 0.3); + background-color: rgba(0, 0, 0, 0.1); + padding: 2%; +} + +#username_label, #username, #password_label, #password { + margin-bottom: 1%; + justify-self: center; +} + +#username, #password { + width: 50%; +} + +#register_buttons { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: 1%; + width: 50%; + justify-self: center; +} + +#error_message_holder { + display: none; +} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..63a27e3 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,93 @@ +html, body { + width: 100%; + height: 100%; + margin: 0; /* Reset default margin */ + padding: 0; /* Reset default padding */ +} + +body { + display: grid; + grid-template-rows: auto auto 2fr auto; + margin: 0; + padding: 0; +} + +/* Set background image */ +body::after { + content: ''; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; /* Ensure the background is behind other content */ + background-image: url("../assets/background.png"); + background-size: cover; + background-repeat: no-repeat; + background-position: center; +} + +header { + display: grid; + width: 100%; + padding-bottom: 0.2%; + justify-content: center; + align-items: center; + font-size: xxx-large; + background-color: rgba(0, 0, 0, 0.1); + color: white; + border-bottom-style: solid; + border-width: thin; + border-color: rgba(0, 0, 0, 0.3); +} + +nav { + display: flex; + justify-content: space-evenly; + justify-self: center; + align-self: center; + margin-top: 1%; + border-style: solid; + border-width: thin; + border-color: rgba(0, 0, 0, 0.3); + border-radius: 2%; + background-color: rgba(0, 0, 0, 0.1); + width: 50%; +} + +a { + text-decoration: none; + color: white; +} + +footer { + display: grid; + height: 100%; + grid-template-rows: 1fr; + grid-template-columns: 1fr 1fr; + background-color: rgba(0, 0, 0, 0.1); + align-self: end; + border-top-style: solid; + border-top-color: rgba(255, 255, 255, 0.1); + border-width: thin; +} + +#flink1 { + display: grid; + grid-column-start: 1; + grid-row-start: 1; + grid-row-end: 1; + grid-column-end: 2; + justify-self: center; + align-self: center; +} + +#flink2 { + display: grid; + grid-column-start: 2; + grid-row-start: 1; + grid-row-end: 1; + grid-column-end: 3; + justify-self: center; + align-self: center; +} \ No newline at end of file diff --git a/static/js/cache.js b/static/js/cache.js new file mode 100644 index 0000000..cee30ed --- /dev/null +++ b/static/js/cache.js @@ -0,0 +1,49 @@ +function addLineBreaks(text) { + // Replace all occurrences of '\n' with '
' + return text.replace(/\n/g, '
'); +} + +// Define decrypt function + +function sendCache() { + + // Declare PSSH variable + let pssh = document.getElementById("cache_pssh"); + + // Declare JSON dictionary and add the values + let json_dict = { + 'PSSH': pssh.value, + } + + // Reset all the fields + pssh.value = '' + + // Set request options + let requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(json_dict) + } + + // Send post request + fetch('/cache', requestOptions) + + // Process return object + .then(response => { + return response.json() + + // Access JSON info + .then(data => { + + // Grab the message + let message = addLineBreaks(data['Message']) + + // Make fields visible and update with message + document.getElementById("cache_results_container").style.display = 'grid' + document.getElementById("results_paragraph").style.justifySelf = 'center' + document.getElementById("cache_results").innerHTML = message + }) + }) +} \ No newline at end of file diff --git a/static/js/devine.js b/static/js/devine.js new file mode 100644 index 0000000..befbe7e --- /dev/null +++ b/static/js/devine.js @@ -0,0 +1,8 @@ +document.addEventListener('DOMContentLoaded', function() { + // Get the current URL from the address bar + var currentURL = window.location.href; + + // Set the current URL value in the paragraph + document.getElementById('currentURL').innerText = currentURL; + document.getElementById('currentURI').innerText = currentURL; +}); \ No newline at end of file diff --git a/static/js/functions.js b/static/js/functions.js new file mode 100644 index 0000000..e69de29 diff --git a/static/js/index.js b/static/js/index.js new file mode 100644 index 0000000..06b82e8 --- /dev/null +++ b/static/js/index.js @@ -0,0 +1,78 @@ +function addLineBreaks(text) { + // Replace all occurrences of '\n' with '
' + return text.replace(/\n/g, '
'); +} + +// Define decrypt function + +function sendDecryption() { + + // Declare PSSH variable + let pssh = document.getElementById("index_pssh"); + + // Declare License URL variable + let license_url = document.getElementById("license_url"); + + // Declare Proxy variable + let proxy = document.getElementById("proxy"); + + // Declare Headers variable + let headers = document.getElementById("headers"); + + // Declare JSON variable + let json = document.getElementById("json"); + + // Declare Cookies variable + let cookies = document.getElementById("cookies"); + + // Declare data variable + let data = document.getElementById("data"); + + // Declare JSON dictionary and add the values + let json_dict = { + 'PSSH': pssh.value, + 'License URL': license_url.value, + 'Headers': headers.value, + 'JSON': json.value, + 'Cookies': cookies.value, + 'Data': data.value, + 'Proxy': proxy.value, + } + + // Reset all the fields + pssh.value = '' + license_url.value = '' + headers.value = '' + json.value = '' + cookies.value = '' + data.value = '' + proxy.value = '' + + // Set request options + let requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(json_dict) + } + + // Send post request + fetch('/', requestOptions) + + // Process return object + .then(response => { + return response.json() + + // Access JSON info + .then(data => { + + // Grab the message + let message = addLineBreaks(data['Message']) + + // Make fields visible and update with message + document.getElementById("results").style.display = 'grid' + document.getElementById("decrypt_results").innerHTML = message + }) + }) +} \ No newline at end of file diff --git a/static/js/login.js b/static/js/login.js new file mode 100644 index 0000000..a4f716d --- /dev/null +++ b/static/js/login.js @@ -0,0 +1,109 @@ +// Login function + +function sendLogin() { + + // Declare Username variable + let username = document.getElementById("username"); + + // Declare Password variable + let password = document.getElementById("password"); + + // Declare JSON dictionary and add the values + let json_dict = { + 'Username': username.value, + 'Password': password.value, + } + + // Set request options + let requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(json_dict) + } + + // Send post request + fetch('/login', requestOptions) + + // Process return object + .then(response => { + return response.json() + + // Access JSON info + .then(data => { + + // Grab the message + let message = data['Message'] + + if (message === 'Failed to Login') { + document.getElementById("error_message_holder").textContent = "Failed to Login" + } + + if (message === 'Username does not exist') { + document.getElementById("error_message_holder").textContent = "Username does not exist" + } + + if (message === 'Success') { + window.location.href = '/profile' + } + }) + }) +} + +// Login function + +function sendRegister() { + + // Declare Username variable + let username = document.getElementById("username"); + + // Declare Password variable + let password = document.getElementById("password"); + + // Declare JSON dictionary and add the values + let json_dict = { + 'Username': username.value, + 'Password': password.value, + } + + // Set request options + let requestOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(json_dict) + } + + // Send post request + fetch('/register', requestOptions) + + // Process return object + .then(response => { + return response.json() + + // Access JSON info + .then(data => { + + // Grab the message + let message = data['Message'] + + if (message === 'Username already taken') { + document.getElementById("error_message_holder").textContent = "Username already taken" + document.getElementById("error_message_holder").style.display = 'grid' + document.getElementById("error_message_holder").style.justifyContent = 'center' + } + + if (message === 'Username cannot be empty') { + document.getElementById("error_message_holder").textContent = "Username cannot be empty" + document.getElementById("error_message_holder").style.display = 'grid' + document.getElementById("error_message_holder").style.justifyContent = 'center' + } + + if (message === 'Success') { + window.location.href = "/profile" + } + }) + }) +} \ No newline at end of file diff --git a/templates/api.html b/templates/api.html new file mode 100644 index 0000000..d17cae0 --- /dev/null +++ b/templates/api.html @@ -0,0 +1,23 @@ + +{% extends "base.html" %} + + +{% block css %} + +{% endblock %} + + +{% block main_body %} +
+ +

API Documentation

+ +
+

Python

+
import requests

CDRM_API = 'https://cdrm-project.com/'

json_data = {
'PSSH': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==',
'License URL': 'https://cwip-shaka-proxy.appspot.com/no_auth',
'Headers': "{'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0'}",
'JSON': "{}",
"Cookies": "{}",
'Data': "{}",
'Proxy': ""
}

decryption_results = requests.post(CDRM_API, json=json_data)

print(decryption_results.json()['Message'])
+
+ +
+ + +{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..a8e1519 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,50 @@ + + + + + + + + + CDRM-Project 2.0 + + {% block css %} + {% endblock %} + + {% block js %} + {% endblock %} + + + + + + + +
+ CDRM-Project 2.0 +
+ + + + + + {% block main_body %} + + {% endblock %} + + + + + + \ No newline at end of file diff --git a/templates/cache.html b/templates/cache.html new file mode 100644 index 0000000..5eb6919 --- /dev/null +++ b/templates/cache.html @@ -0,0 +1,44 @@ + +{% extends "base.html" %} + + +{% block css %} + +{% endblock %} + + +{% block js %} + +{% endblock %} + + +{% block main_body %} + + +
+ + +

Check the cache

+ + +
+

Total Keys: {{ cache_page_key_count }}

+ +
+ +
+ +
+ +
+

+ RESULTS +

+

+ +

+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/devine.html b/templates/devine.html new file mode 100644 index 0000000..62b7f48 --- /dev/null +++ b/templates/devine.html @@ -0,0 +1,32 @@ + +{% extends "base.html" %} + + +{% block css %} + +{% endblock %} + + +{% block js %} + +{% endblock %} + + +{% block main_body %} +
+ +

Devine

+

+ Devine is a Modular Movie, TV, and Music Archival Software, service scripts can be found on The CDM-Project +
+ Along with the extension, CDRM-Project is compatible with devine's remote CDM option out of the box, add this to your devine.yaml +

+
+

remote_cdm:
    - name: "CDRM_Project_API"
      device_type: ANDROID
      system_id: {{ cdm_version }}
      security_level: 3
      host: ""
      secret: "CDRM-Project"
      device_name: "CDM"

+

key_vaults:
    - type: API
      name: "CDRM-Vault"
      token: ""
      uri: "/vault"

+

Devine keys cached: {{ devine_key_count }}
Unique services cached: {{ devine_service_count }}

+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/extension.html b/templates/extension.html new file mode 100644 index 0000000..db9258c --- /dev/null +++ b/templates/extension.html @@ -0,0 +1,26 @@ + +{% extends "base.html" %} + + +{% block css %} + +{% endblock %} + + +{% block main_body %} +
+

WVGuesserExtension-NextGen: CDRM-Edition

+ +
+
+            Slightly modified version of FoxRefire's extension WVGuesserExtension-NextGen
+            
+ How to use: Follow step 3 from the original github page linked above with the version of the extension down below, that's it! No configuration needed. +
+ +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/faq.html b/templates/faq.html new file mode 100644 index 0000000..96b17b0 --- /dev/null +++ b/templates/faq.html @@ -0,0 +1,54 @@ + +{% extends "base.html" %} + + +{% block css %} + +{% endblock %} + + +{% block main_body %} + + +
+ + +

FAQ

+ + +
+ +
+ + What is this site? + +

+ An open source web application built with python/flask to decrypt L3 Widevine protected content as well as an alternative to GetWVKeys. +

+
+ +
+ + Whoa redesign, what happened? + +

+ While the old site was pretty cool, it wasn't written by me - It was code leaked from an old version of GetWVKeys +

+ I thought it was about time that changed. +

+
+ +
+ + Need help? + +

+ Join the discord! https://discord.gg/CDRM-Project +

+
+ +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a232631 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,56 @@ + +{% extends "base.html" %} + + +{% block css %} + +{% endblock %} + + +{% block js %} + +{% endblock %} + + +{% block main_body %} + + +
+ + +

Send Widevine license request

+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+

RESULTS

+

+
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..bf46b7c --- /dev/null +++ b/templates/login.html @@ -0,0 +1,37 @@ + +{% extends "base.html" %} + + +{% block css %} + +{% endblock %} + + +{% block js %} + +{% endblock %} + + +{% block main_body %} +
+ +
+ +
+ +
+ +
+ +
+
+ + +
+

Error message holder

+
+ +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..214d6b2 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,16 @@ + +{% extends "base.html" %} + + +{% block css %} + +{% endblock %} + + +{% block main_body %} +
+ +
+ + +{% endblock %} \ No newline at end of file