diff --git a/Helpers/api_check.py b/Helpers/api_check.py index 95b268b..184f9dc 100644 --- a/Helpers/api_check.py +++ b/Helpers/api_check.py @@ -1,6 +1,5 @@ # Import dependencies import os -from sys import exit # Define api key check diff --git a/Helpers/download.py b/Helpers/download.py index 22ad1e0..cb5694e 100644 --- a/Helpers/download.py +++ b/Helpers/download.py @@ -29,11 +29,15 @@ def web_dl_generic(mpd: str = None, device: str = None, api_key: str = None, rem mp4decrypt_keys, _ = Sites.Generic.decrypt_generic(mpd_url=mpd, wvd=device, license_curl_headers=license_curl.headers) if site == 'rte': mp4decrypt_keys, _ = Sites.RTE.decrypt_rte(mpd_url=mpd, wvd=device, license_curl_headers=license_curl.headers) + if site == 'udemy': + mp4decrypt_keys, _ = Sites.Udemy.decrypt_udemy(mpd_url=mpd, wvd=device, license_curl_headers=license_curl.headers, license_curl_cookies=license_curl.cookies) if remote: if not site: mp4decrypt_keys, _ = Sites.Generic.decrypt_generic_remotely(api_key=api_key, license_curl_headers=license_curl.headers, mpd_url=mpd) if site == 'rte': mp4decrypt_keys, _ = Sites.RTE.decrypt_rte_remotely(mpd_url=mpd, api_key=api_key, license_curl_headers=license_curl.headers) + if site == 'udemy': + mp4decrypt_keys, _ = Sites.Udemy.decrypt_udemy_remotely(mpd_url=mpd, api_key=api_key, license_curl_headers=license_curl.headers, license_curl_cookies=license_curl.cookies) # Define n_m3u8dl-re download parameters n_m3u8dl_re_download = [ diff --git a/Helpers/gui.py b/Helpers/gui.py index 5303def..1edb848 100644 --- a/Helpers/gui.py +++ b/Helpers/gui.py @@ -41,15 +41,15 @@ def start_gui(wvd: str = None, api_key: str = None): [sg.Multiline(key='-COOKIES-', visible=False, expand_x=True, expand_y=True, tooltip=f"Paste cookies dictionary starting with the first curly brace '{{' and ending with the last curly brace '}}'")], [sg.Text('releasePid:', key='-PID_TEXT-', visible=False)], [sg.Input(key='-PID-', visible=False, expand_x=True, expand_y=False)], - [sg.Combo(values=['Generic', 'Crunchyroll', 'YouTube', 'RTE'], default_value='Generic', key='-OPTIONS-', + [sg.Combo(values=['Generic', 'Crunchyroll', 'YouTube', 'RTE', 'Udemy'], default_value='Generic', key='-OPTIONS-', enable_events=True), sg.Push(), sg.Checkbox(text="Use CDM-Project API", key='-USE_API-')] ] if wvd is None and api_key is not None: - right_frame[8] = [sg.Combo(values=['Generic', 'Crunchyroll', 'YouTube', 'RTE'], default_value='Generic', key='-OPTIONS-', + right_frame[8] = [sg.Combo(values=['Generic', 'Crunchyroll', 'YouTube', 'RTE', 'Udemy'], default_value='Generic', key='-OPTIONS-', enable_events=True), sg.Push(), sg.Checkbox(text="Use CDM-Project API", key='-USE_API-', default=True, disabled=True)] if api_key is None and wvd is not None: - right_frame[8] = [sg.Combo(values=['Generic', 'Crunchyroll', 'YouTube', 'RTE'], default_value='Generic', key='-OPTIONS-', + right_frame[8] = [sg.Combo(values=['Generic', 'Crunchyroll', 'YouTube', 'RTE', 'Udemy'], default_value='Generic', key='-OPTIONS-', enable_events=True), sg.Push(), sg.Checkbox(text="Use CDM-Project API", key='-USE_API-', default=False, disabled=True)] right_frame_normal = sg.Col(right_frame, expand_x=True, expand_y=True) @@ -65,7 +65,7 @@ def start_gui(wvd: str = None, api_key: str = None): # the event loop while True: if wvd is None and api_key is None: - sg.popup(title="TPD-Keys", custom_text="No CDM or API key found!") + sg.popup('Error!', 'No CDM or API Key found', custom_text="Close", icon=assets.images.taskbar) break event, values = window.read() @@ -236,6 +236,46 @@ def start_gui(wvd: str = None, api_key: str = None): if values['-PSSH-'] != '' and values['-LIC_URL-'] != '' and values['-PID-'] == '' and values['-OPTIONS-'] == 'RTE': window['-OUTPUT-'].update(f"No PID provided") + # Action for Decrypt for Udemy decrypt if fields are filled out + if values['-PSSH-'] != '' and values['-OPTIONS-'] == 'Udemy' and values['-LIC_URL-'] != '' and values['-HEADERS-'] != '' and values['-COOKIES-'] != '': + if not values['-USE_API-']: + try: + _, key_out = Sites.Udemy.decrypt_udemy(wvd=wvd, in_pssh=values['-PSSH-'], + license_curl_headers=ast.literal_eval(clean_dict(dict=values['-HEADERS-'])), + license_curl_cookies=ast.literal_eval(clean_dict(dict=values['-COOKIES-']))) + window['-OUTPUT-'].update(f"{key_out}") + except Exception as error: + window['-OUTPUT-'].update(f"{error}") + if values['-USE_API-']: + if api_key is None: + window['-OUTPUT-'].update(f"No API key") + if api_key is not None: + try: + _, key_out = Sites.Udemy.decrypt_udemy_remotely(api_key=api_key, in_pssh=values['-PSSH-'], + license_curl_headers=ast.literal_eval(clean_dict(dict=values['-HEADERS-'])), + license_curl_cookies=ast.literal_eval(clean_dict(dict=values['-COOKIES-']))) + window['-OUTPUT-'].update(f"{key_out}") + except Exception as error: + window['-OUTPUT-'].update(f"{error}") + + # Error for no PSSH - Udemy + if values['-PSSH-'] == '' and values['-OPTIONS-'] == 'Udemy': + window['-OUTPUT-'].update(f"No PSSH provided") + + # Error for no License URL - Udemy + if values['-LIC_URL-'] == '' and values['-OPTIONS-'] == 'Udemy': + window['-OUTPUT-'].update(f"No License URL provided") + + # Error for no Headers - Udemy + if values['-PSSH-'] != '' and values['-LIC_URL-'] != '' and values['-OPTIONS-'] == 'Udemy' and values['-HEADERS-'] == '': + window['-OUTPUT-'].update(f"No Headers provided") + + # Error for no Cookies - Udemy + if values['-PSSH-'] != '' and values['-LIC_URL-'] != '' and values['-OPTIONS-'] == 'Udemy' and values['-COOKIES-'] == '': + window['-OUTPUT-'].update(f"No Cookies provided") + + + # Actions for reset button if event == 'Reset': @@ -291,6 +331,19 @@ def start_gui(wvd: str = None, api_key: str = None): window['-PID_TEXT-'].update(visible=True) window['-PID-'].update(visible=True, value="") + # Actions for Udemy selector + if event == '-OPTIONS-' and values['-OPTIONS-'] == 'Udemy': + window['-PSSH-'].update(value="", disabled=False) + window['-LIC_URL-'].update(value="", disabled=False) + window['-HEADERS_TEXT-'].update(visible=True) + window['-HEADERS-'].update(value="", visible=True) + window['-JSON-'].update(value="", visible=False) + window['-JSON_TEXT-'].update(visible=False) + window['-COOKIES-'].update(value="", visible=True) + window['-COOKIES_TEXT-'].update(visible=True) + window['-PID_TEXT-'].update(visible=False) + window['-PID-'].update(visible=False, value="") + # Actions for YouTube selector if event == '-OPTIONS-' and values['-OPTIONS-'] == 'YouTube': window['-PSSH-'].update(value="", disabled=True) @@ -314,7 +367,7 @@ def start_gui(wvd: str = None, api_key: str = None): if event == 'Source Code': webbrowser.open(url='https://cdm-project.com/Decryption-Tools/TPD-Keys') if event == 'Version': - sg.popup('Version 1.3', custom_text='Close', grab_anywhere=True) + sg.popup('Version 1.31', custom_text='Close', grab_anywhere=True) # 4 - the close window.close() diff --git a/Helpers/mpd_parse.py b/Helpers/mpd_parse.py index 745b91a..82cb795 100644 --- a/Helpers/mpd_parse.py +++ b/Helpers/mpd_parse.py @@ -4,10 +4,10 @@ from sys import exit # Define MPD / m3u8 PSSH parser -def parse_pssh(manifest_url): +def parse_pssh(manifest_url, license_headers: dict = None): manifest = manifest_url try: - response = requests.get(manifest) + response = requests.get(manifest, headers=license_headers) except: pssh = input("Couldn't retrieve manifest, please input PSSH: ") return pssh diff --git a/Helpers/os_check.py b/Helpers/os_check.py index b3c9c13..6b70249 100644 --- a/Helpers/os_check.py +++ b/Helpers/os_check.py @@ -1,5 +1,4 @@ import os -from sys import exit def get_os_specific(): diff --git a/Helpers/wvd_check.py b/Helpers/wvd_check.py index c7f7cdb..61839fe 100644 --- a/Helpers/wvd_check.py +++ b/Helpers/wvd_check.py @@ -1,7 +1,6 @@ # Import dependencies import os import glob -from sys import exit # Define WVD device check diff --git a/Sites/Crunchyroll.py b/Sites/Crunchyroll.py index 6852e09..1b9da17 100644 --- a/Sites/Crunchyroll.py +++ b/Sites/Crunchyroll.py @@ -20,7 +20,7 @@ def decrypt_crunchyroll(wvd: str = None, license_curl_headers: dict = None, mpd_ # Try getting pssh via MPD URL if web-dl if mpd_url is not None: - input_pssh = Helpers.mpd_parse.parse_pssh(mpd_url) + input_pssh = Helpers.mpd_parse.parse_pssh(mpd_url, license_headers=license_curl_headers) if input_pssh is not None: print(f'\nPSSH found: {input_pssh}') else: @@ -135,7 +135,7 @@ def decrypt_crunchyroll_remotely(api_key: str = None, license_curl_headers: dict # Try getting pssh via MPD URL if web-dl if mpd_url is not None and in_pssh is None: - input_pssh = Helpers.mpd_parse.parse_pssh(mpd_url) + input_pssh = Helpers.mpd_parse.parse_pssh(mpd_url, license_headers=license_curl_headers) if input_pssh is not None: print(f'\nPSSH found: {input_pssh}') else: @@ -191,6 +191,7 @@ def decrypt_crunchyroll_remotely(api_key: str = None, license_curl_headers: dict if license.status_code != 200: print(f"An error occurred!\n{license.content}") + return None, license.content # Retrieve the license message license = license.json()["license"] diff --git a/Sites/Udemy.py b/Sites/Udemy.py new file mode 100644 index 0000000..f779020 --- /dev/null +++ b/Sites/Udemy.py @@ -0,0 +1,249 @@ +# Import dependencies + +from pywidevine import PSSH +from pywidevine import Cdm +from pywidevine import Device +import requests +import base64 +import os +import Helpers +from sys import exit + + +# Defining decrypt function for generic services +def decrypt_udemy(wvd: str = None, license_curl_headers: dict = None, license_curl_cookies: str = None, mpd_url: str = None, + in_pssh: str = None, in_license_url: str = None): + + # Exit if no device + if wvd is None: + exit(f"No CDM! to use local decryption place a .wvd in {os.getcwd()}/WVDs") + + # Try getting pssh via MPD URL if web-dl + if mpd_url is not None: + input_pssh = Helpers.mpd_parse.parse_pssh(mpd_url) + if input_pssh is not None: + print(f'\nPSSH found: {input_pssh}') + else: + input_pssh = input(f"\nPSSH not found! Input PSSH: ") + + # Ask for PSSH if just keys function + if mpd_url is None and in_pssh is None: + # Ask for PSSH if web-dl not selected: + input_pssh = input(f"\nPSSH: ") + + # prepare pssh + if in_pssh is None: + try: + pssh = PSSH(input_pssh) + except Exception as error: + print(f'an error occurred!\n{error}') + return None, error + if in_pssh is not None: + try: + pssh = PSSH(in_pssh) + except Exception as error: + print(f'an error occurred!\n{error}') + return None, error + + # Ask for license URL + if in_license_url is None: + license_url = input(f"\nLicense URL: ") + if in_license_url is not None: + license_url = in_license_url + + # load device + device = Device.load(wvd) + + # load CDM from device + cdm = Cdm.from_device(device) + + # open CDM session + session_id = cdm.open() + + # generate license challenge + challenge = cdm.get_license_challenge(session_id, pssh) + + # send license challenge + license = requests.post( + url=license_url, + data=challenge, + headers=license_curl_headers, + cookies=license_curl_cookies + ) + + if license.status_code != 200: + print(f'An error occurred!\n{license.content}') + return license.content + + # Extract license from content + license = license.content + + # parse license challenge + try: + cdm.parse_license(session_id, license) + except Exception as error: + print(f'an error occurred!\n{error}') + return None, error + + # 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" + + # assign variable for mp4decrypt keys + mp4decrypt_keys = [] + for key in cdm.get_keys(session_id): + if key.type != "SIGNING": + mp4decrypt_keys.append('--key') + mp4decrypt_keys.append(f'{key.kid.hex}:{key.key.hex()}') + + # close session, disposes of session data + cdm.close(session_id) + + # Cache the keys + if in_pssh is None: + Helpers.cache_key.cache_keys(pssh=input_pssh, keys=returned_keys) + if in_pssh is not None: + Helpers.cache_key.cache_keys(pssh=in_pssh, keys=returned_keys) + + # Print out the keys + print(f'\nKeys:\n{returned_keys}') + + # Return the keys for future ripper use. + return mp4decrypt_keys, returned_keys + + +# Defining remote decrypt function for generic services +def decrypt_udemy_remotely(api_key: str = None, license_curl_headers: dict = None, license_curl_cookies: dict = None, mpd_url: str = None, + in_pssh: str = None, in_license_url: str = None): + + # Exit if no API key + if api_key is None: + exit(f"No API Key! to use remote decryption place an API key in {os.getcwd()}/Config/api-key.txt") + + # Set CDM Project API URL + api_url = "https://api.cdm-project.com" + + # Set API device + api_device = "CDM" + + # Try getting pssh via MPD URL if web-dl + if mpd_url is not None and in_pssh is None: + input_pssh = Helpers.mpd_parse.parse_pssh(mpd_url) + if input_pssh is not None: + print(f'\nPSSH found: {input_pssh}') + else: + input_pssh = input(f"\nPSSH not found! Input PSSH: ") + + # Ask for PSSH if just keys function + if mpd_url is None and in_pssh is None: + # Ask for PSSH if web-dl not selected: + input_pssh = input(f"\nPSSH: ") + + if in_pssh is not None: + input_pssh = in_pssh + + # Ask for license URL + if in_license_url is None: + input_license_url = input(f"\nLicense URL: ") + if in_license_url is not None: + input_license_url = in_license_url + + # Set headers for API key + api_key_headers = { + "X-Secret-Key": api_key + } + + # Open CDM session + open_session = requests.get(url=f'{api_url}/{api_device}/open', headers=api_key_headers) + + # Error handling + if open_session.status_code != 200: + print(f"An error occurred!\n{open_session.content}") + return None, open_session.content + + # Get the session ID from the open CDM session + session_id = open_session.json()["data"]["session_id"] + + # Set JSON required to generate a license challenge + generate_challenge_json = { + "session_id": session_id, + "init_data": input_pssh + } + + # Generate the license challenge + generate_challenge = requests.post(url=f'{api_url}/{api_device}/get_license_challenge/AUTOMATIC', headers=api_key_headers, json=generate_challenge_json) + + # Error handling + if generate_challenge.status_code != 200: + print(f"An error occurred!\n{generate_challenge.content}") + return None, generate_challenge.content + + # Retrieve the challenge and base64 decode it + challenge = base64.b64decode(generate_challenge.json()["data"]["challenge_b64"]) + + # Send the challenge to the widevine license server + license = requests.post( + url=input_license_url, + headers=license_curl_headers, + cookies=license_curl_cookies, + data=challenge + ) + + if license.status_code != 200: + print(f'An error occurred!\n{license.content}') + return None, license.content + + # Retrieve the license message + license = base64.b64encode(license.content).decode() + + # Set JSON required to parse license message + license_message_json = { + "session_id": session_id, + "license_message": license + } + + # Parse the license + parse = requests.post(url=f'{api_url}/{api_device}/parse_license', headers=api_key_headers, json=license_message_json) + + # Error handling + if parse.status_code != 200: + print(f"An error occurred!\n{parse.content}") + return None, parse.content + + # Retrieve the keys + get_keys = requests.post(url=f'{api_url}/{api_device}/get_keys/ALL', + json={"session_id": session_id}, + headers=api_key_headers) + + # Error handling + if get_keys.status_code != 200: + print(f"An error occurred!\n{get_keys.content}") + return None, get_keys.content + + # Iterate through the keys, ignoring signing key + returned_keys = '' + for key in get_keys.json()["data"]["keys"]: + if not key["type"] == "SIGNING": + returned_keys += f"{key['key_id']}:{key['key']}\n" + + # assign variable for mp4decrypt keys + mp4decrypt_keys = [] + for key in get_keys.json()["data"]["keys"]: + if not key["type"] == "SIGNING": + mp4decrypt_keys.append('--key') + mp4decrypt_keys.append(f"{key['key_id']}:{key['key']}") + + # Cache the keys + Helpers.cache_key.cache_keys(pssh=input_pssh, keys=returned_keys) + + + # Print out keys + print(f'\nKeys:\n{returned_keys}') + + # Close session + requests.get(url=f'{api_url}/{api_device}/close/{session_id}', headers=api_key_headers) + + # Return mp4decrypt keys + return mp4decrypt_keys, returned_keys diff --git a/Sites/__init__.py b/Sites/__init__.py index fc11a20..c2c7ace 100644 --- a/Sites/__init__.py +++ b/Sites/__init__.py @@ -4,4 +4,5 @@ from . import YouTube from . import VDOCipher from . import Canal from . import RTE -from . import AstroGo \ No newline at end of file +from . import AstroGo +from . import Udemy \ No newline at end of file diff --git a/license_curl.py b/license_curl.py index a149b6c..e69de29 100644 --- a/license_curl.py +++ b/license_curl.py @@ -1 +0,0 @@ -headers = {} \ No newline at end of file diff --git a/tpd-keys.py b/tpd-keys.py index 9d6a7b8..59803c6 100644 --- a/tpd-keys.py +++ b/tpd-keys.py @@ -21,8 +21,11 @@ services.add_argument('--crunchyroll', action='store_true', help="Decrypt Crunch services.add_argument('--crunchyroll-remote', action='store_true', help="Decrypt Crunchyroll remotely") services.add_argument('--generic', action='store_true', help="Decrypt generic services") services.add_argument('--generic-remote', action='store_true', help="Decrypt generic services remotely") +services.add_argument('--pssh', action='store_true', help="Parse a PSSH from an MPD link.") services.add_argument('--rte', action='store_true', help="Decrypt RTE") services.add_argument('--rte-remote', action='store_true', help="Decrypt RTE remotely") +services.add_argument('--udemy', action='store_true', help="Decrypt Udemy") +services.add_argument('--udemy-remote', action='store_true', help="Decrypt Udemy remotely") services.add_argument('--youtube', action='store_true', help="Decrypt YouTube") services.add_argument('--youtube-remote', action='store_true', help="Decrypt YouTube remotely") @@ -72,9 +75,14 @@ elif switches.generic_remote: else: Sites.Generic.decrypt_generic_remotely(api_key=api_key, license_curl_headers=license_curl.headers) +elif switches.pssh: + # Perform action for --pssh + mpd = input("MPD URL: ") + print(f'PSSH: {Helpers.mpd_parse.parse_pssh(manifest_url=mpd)}') + elif switches.rte: - # Perform action for --rte , perform a default action + # Perform action for --rte if switches.web_dl: mpd = input("MPD URL: ") file = Helpers.download.web_dl_generic(mpd=mpd, device=device, site='rte') @@ -92,6 +100,25 @@ elif switches.rte_remote: else: Sites.RTE.decrypt_rte_remotely(api_key=api_key) +elif switches.udemy: + # Perform action for --udemy + if switches.web_dl: + mpd = input("MPD URL: ") + file = Helpers.download.web_dl_generic(mpd=mpd, device=device, site='udemy') + print(f'Saved at {file[0]}') + else: + Sites.Udemy.decrypt_udemy(wvd=device, license_curl_headers=license_curl.headers, license_curl_cookies=license_curl.cookies) + + +elif switches.udemy_remote: + # Perform action for --udemy-remote + if switches.web_dl: + mpd = input("MPD URL: ") + file = Helpers.download.web_dl_generic(mpd=mpd, api_key=api_key, remote=True, site='udemy') + print(f'Saved at {file[0]}') + else: + Sites.Udemy.decrypt_udemy_remotely(api_key=api_key, license_curl_headers=license_curl.headers, license_curl_cookies=license_curl.cookies) + elif switches.youtube: # Perform action for --YouTube