diff --git a/.gitignore b/.gitignore index 3f670e5..04687f7 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,7 @@ cover/ *.pot # Django stuff: -# *.log +*.log local_settings.py db.sqlite3 db.sqlite3-journal diff --git a/CHANGELOG.md b/CHANGELOG.md index 6015ee4..2915d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,39 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.0] - 2024-07-06 + +### Added + +- Support for custom library names. +- Patch for the `GetCdmClientPropertySet` function to enforce unencrypted challenges on specific devices. +- Hook for the `getOemcryptoDeviceId` function for compatible devices. +- Native C API filter to prevent crashes during hooks. +- Handling of a binary challenge file to aid in resolving client IDs. +- Optional verbosity with output files if specified. +- Process watcher for library resolution, primarily for older devices. + +### Changed + +- Removed display of OEM and library version as they were often incorrect. +- New patch method (rewriting) to enforce unencrypted challenges. +- Widevine detection method now based on process names. +- Program no longer stops when the function file is not used when normally required. +- Full path display of the library (instead of parent only). +- Program is now formatted as a library. +- Simplified symbol address resolution. +- New, more relevant output structure. +- Private key size is no longer recalculated. + +### Fixed + +- Dynamic detection of the argument position for challenges. +- Corrected path for extracted files. +- Backward-compatible support for listing all processes via ADB. +- Fixed automatic mode device usage. +- Support for parsing errors related to CDM data. +- Frida session closure after process analysis. + ## [1.1.0] - 2024-06-22 ### Added @@ -160,6 +193,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Initial release of the project, laying the foundation for future enhancements and features. +[2.0.0]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.0.0 [1.1.0]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.1.0 [1.0.9]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.9 [1.0.8]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.8 diff --git a/README.md b/README.md index 83bb9d4..5b8af53 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,36 @@ # KeyDive: Widevine L3 Extractor for Android -KeyDive is a sophisticated Python script designed for the precise extraction of Widevine L3 DRM (Digital Rights Management) keys from Android devices. This tool leverages the capabilities of the Widevine CDM (Content Decryption Module) to facilitate the recovery of DRM keys, enabling a deeper understanding and analysis of the Widevine L3 DRM implementation across various Android SDK versions. +KeyDive is a sophisticated Python script designed for precise extraction of Widevine L3 DRM (Digital Rights Management) keys from Android devices. This tool leverages the capabilities of the Widevine CDM (Content Decryption Module) to facilitate the recovery of DRM keys, enabling a deeper understanding and analysis of the Widevine L3 DRM implementation across various Android SDK versions. > [!IMPORTANT] > -> Support for Android 14+ (SDK > 33) require the use of functions extracted from Ghidra. +> Support for OEM API 18+ (SDK > 33) require the use of functions extracted from Ghidra. ## Features -- Automated extraction of Widevine L3 DRM keys. +- **Automated extraction** of Widevine L3 DRM keys. - Compatibility with a wide range of Android versions (SDK > 22), ensuring broad applicability. - Seamless extraction process, yielding essential DRM components such as the `client_id.bin` for device identification and the `private_key.pem` for the RSA private key. +- **Offline extraction mode** for situations without internet access. +- Command-line options for flexibility in usage. +- Support for custom functions extracted from Widevine libraries using Ghidra. ## Prerequisites Before you begin, ensure you have the following prerequisites in place: -1. **ADB (Android Debug Bridge):** Make sure to install [ADB](https://developer.android.com/studio/command-line/adb) and include it in your system's PATH environment variable for easy command-line access. +1. **ADB (Android Debug Bridge):** Make sure to install [ADB](https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge) and include it in your system's PATH environment variable for easy command-line access. 2. **Frida-Server:** Install `frida-server` on your target Android device. This requires root access on the device. For installation instructions and downloads, visit the [official Frida documentation](https://frida.re/docs/installation/). -3. **Python Requirements:** KeyDive requires specific Python libraries to function correctly. Install them using the provided `requirements.txt` file: - ```shell - pip install -r requirements.txt - ``` ## Installation Follow these steps to set up KeyDive: 1. Ensure all prerequisites are met (see above). -2. Clone this repository to your local machine. -3. Navigate to the cloned directory and install the required Python dependencies as mentioned. +2. Install KeyDive from PyPI using Poetry: + ```shell + pip install keydive + ``` ## Usage @@ -52,24 +53,36 @@ For situations where internet access is limited or unavailable, KeyDive supports 2. **Execute KeyDive in Offline Mode:** - Once all the preparations are complete and the device is disconnected from the internet, run the KeyDive script to extract the Widevine L3 keys. Ensure that the DRM-protected content is ready and available on the device for extraction. -For a detailed step-by-step guide on setting up and executing KeyDive without internet access, please refer to our dedicated document: [Offline Mode Detailed Guide](./docs/Axinom/OFFLINE.md). +For a detailed step-by-step guide on setting up and executing KeyDive without internet access, please refer to our dedicated document: [Offline Mode Detailed Guide](./docs/axinom/OFFLINE.md). ### Command-Line Options ```shell -usage: keydive.py [-h] [-a] [-d DEVICE] [-f FUNCTIONS] [-w] [--force] +usage: keydive [-h] [-d ] [-v] [-l ] [--delay ] [--version] [-a] [-c ] [-w] [-o ] [-f ] Extract Widevine L3 keys from an Android device. options: -h, --help show this help message and exit - -a, --auto Open Bitmovin's demo automatically. - -d DEVICE, --device DEVICE - Target Android device ID. - -f FUNCTIONS, --functions FUNCTIONS - Path to Ghidra XML functions file. + +Global options: + -d , --device + Specify the target Android device ID to connect with via ADB. + -v, --verbose Enable verbose logging for detailed debug output. + -l , --log + Directory to store log files. + --delay Delay (in seconds) between process checks in the watcher. + --version Display KeyDive version information. + +Cdm options: + -a, --auto Automatically open Bitmovin's demo. + -c , --challenge + Path to unencrypted challenge for extracting client ID. -w, --wvd Generate WVD. - --force Force using the default vendor (skipping analysis). + -o , --output + Output directory path for extracted data. + -f , --functions + Path to Ghidra XML functions file. ``` @@ -79,11 +92,11 @@ For advanced users looking to use custom functions with KeyDive, a comprehensive ## Temporary Disabling L1 for L3 Extraction -Some manufacturers (e.g., Xiaomi) allow the use of L1 keyboxes even after unlocking the bootloader. In such cases, it's necessary to install a Magisk module called [liboemcrypto-disabler](https://github.com/hzy132/liboemcryptodisabler) to temporarily disable L1, thereby facilitating L3 key extraction. +Some manufacturers (e.g., Xiaomi) allow the use of L1 keyboxes even after unlocking the bootloader. In such cases, it's necessary to install a Magisk module called [liboemcrypto-disabler](https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#liboemcrypto-disabler) to temporarily disable L1, thereby facilitating L3 key extraction. ## Credits -Special thanks to the original developers and contributors who have made KeyDive possible. This tool is the culmination of collaborative efforts, research, and a deep understanding of DRM technologies. +Special thanks to the original developers and contributors who have made KeyDive possible. This tool is the culmination of collaborative efforts, research, and a deep understanding of DRM technologies. ## Disclaimer diff --git a/docs/Axinom/axinom_rsa.p12 b/docs/Axinom/axinom_rsa.p12 new file mode 100644 index 0000000..965dc3d Binary files /dev/null and b/docs/Axinom/axinom_rsa.p12 differ diff --git a/docs/Axinom/patch/axinom_signed.apk b/docs/Axinom/patch/axinom_signed.apk new file mode 100644 index 0000000..9641ec3 Binary files /dev/null and b/docs/Axinom/patch/axinom_signed.apk differ diff --git a/docs/Axinom/patch/config.yaml b/docs/Axinom/patch/config.yaml new file mode 100644 index 0000000..4564d30 --- /dev/null +++ b/docs/Axinom/patch/config.yaml @@ -0,0 +1,26 @@ +keystore: + algo: RSA + alias: Axinom_DRM_DEMO + meta: + common_name: Axinom + country: EE + locality: Tartu + organization: Axinom + organizational_unit: Front-End + state: Tartumaa + password: Axinom_PASSWORD + sign: SHA-256 + size: 2048 + validity: 9125 +metadata: + input: axinom.apk + output: axinom_signed.apk + source: https://github.com/Axinom/drm-sample-player-android + version: '202211021' +patch: + assets/samplelist.json: + - - null + - samplelist.json + com/axinom/drm/sample/activity/SampleChooserActivity.smali: + - - SampleChooserActivity.smali + - null diff --git a/extractor/__init__.py b/extractor/__init__.py deleted file mode 100644 index 0043da0..0000000 --- a/extractor/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .cdm import * -from .vendor import * - -__version__ = '1.1.0' diff --git a/extractor/cdm.py b/extractor/cdm.py deleted file mode 100644 index 96c651c..0000000 --- a/extractor/cdm.py +++ /dev/null @@ -1,321 +0,0 @@ -import json -import logging -import re -import subprocess -from pathlib import Path -from zlib import crc32 - -import xmltodict -import frida -from frida.core import Device, Session, Script -from Cryptodome.PublicKey import RSA - -from pywidevine.device import Device, DeviceTypes -from pywidevine.license_protocol_pb2 import SignedMessage, LicenseRequest, ClientIdentification, DrmCertificate, SignedDrmCertificate -from unidecode import unidecode - -from extractor.constants import Native -from extractor.uils import sanitize -from extractor.vendor import Vendor - -PARENT = Path(__file__).parent - - -class Cdm: - """ - Manages the capture and processing of DRM keys from a specified device using Frida to inject custom hooks. - """ - OEM_CRYPTO_API = { - # Mapping of function names across different API levels (obfuscated names may vary). - 'rnmsglvj', 'polorucp', 'kqzqahjq', 'pldrclfq', 'kgaitijd', - 'cwkfcplc', 'crhqcdet', 'ulns', 'dnvffnze', 'ygjiljer', - 'qbjxtubz', 'qkfrcjtw', 'rbhjspoh', 'zgtjmxko', 'igrqajte', - 'ofskesua', 'qllcoacg', 'pukctkiv', 'ehmduqyt' - # Add more as needed for different versions. - } - - def __init__(self, device: str = None, functions: Path = None, force: bool = False, wvd: bool = False): - self.logger = logging.getLogger('Cdm') - self.functions = functions - self.running = True - self.keys = {} - # Select device based on provided ID or default to the first USB device. - self.device: Device = frida.get_device(id=device, timeout=5) if device else frida.get_usb_device(timeout=5) - self.logger.info('Device: %s (%s)', self.device.name, self.device.id) - - # Select if create WVD or not - self.wvd = wvd - - # Obtain device properties - self.properties = self._fetch_device_properties() - - self.sdk_api = self.properties['ro.build.version.sdk'] - self.logger.info('SDK API: %s', self.sdk_api) - self.logger.info('ABI CPU: %s', self.properties['ro.product.cpu.abi']) - - # Load the hook scrip - self.script = self._prepare_hook_script() - self.logger.info('Script loaded successfully') - - # Determine vendor based on device SDK API - vendor_api = self._prepare_vendor_api(force=force) - self.vendor = Vendor.from_sdk_api(vendor_api) - - # Update script for specific vendor API, if necessary - if vendor_api != self.sdk_api: - self.sdk_api = vendor_api - self.script = self._prepare_hook_script() - self.logger.info('Script updated for vendor API') - - def _fetch_device_properties(self) -> dict: - """ - Retrieves system properties from the connected device using ADB shell commands. - """ - # https://source.android.com/docs/core/architecture/configuration/add-system-properties?#shell-commands - properties = {} - sp = subprocess.run(['adb', '-s', self.device.id, 'shell', 'getprop'], capture_output=True) - for line in sp.stdout.decode('utf-8').splitlines(): - match = re.match(r'\[(.*?)\]: \[(.*?)\]', line) - if match: - key, value = match.groups() - # Attempt to cast numeric and boolean values to appropriate types - try: - value = int(value) - except ValueError: - if value.lower() in ('true', 'false'): - value = value.lower() == 'true' - properties[key] = value - return properties - - def _prepare_hook_script(self) -> str: - """ - Prepares the Frida hook script, injecting dynamic content like SDK API and selected functions. - """ - content = (PARENT / 'keydive.js').read_text(encoding='utf-8') - selected = self._select_functions() if self.functions else {} - - # Replace placeholders in script template - replacements = { - '${SDK_API}': str(self.sdk_api), - '${OEM_CRYPTO_API}': json.dumps(list(self.OEM_CRYPTO_API)), - '${NATIVE_C_API}': json.dumps([v for i in list(Native) for v in i.value]), - '${SYMBOLS}': json.dumps(list(selected.values())), - } - - for placeholder, real_value in replacements.items(): - content = content.replace(placeholder, real_value) - - return content - - def _select_functions(self) -> dict: - """ - Parses the provided XML functions file to select relevant functions. - """ - if not self.functions.is_file(): - raise FileNotFoundError('Functions file not found') - - try: - program = xmltodict.parse(self.functions.read_bytes())['PROGRAM'] - addr_base = int(program['@IMAGE_BASE'], 16) - functions = program['FUNCTIONS']['FUNCTION'] - - # Find a target function from a predefined list - target = next((f['@NAME'] for f in functions if f['@NAME'] in self.OEM_CRYPTO_API), None) - - # Extract relevant functions - selected = {} - for func in functions: - name = func['@NAME'] - args = len(func.get('REGISTER_VAR', [])) - - # Add function if it matches specific criteria - if name not in selected and ( - name == target - or any(keyword in name for keyword in ['UsePrivacyMode', 'PrepareKeyRequest']) - or (not target and re.match(r'^[a-z]+$', name) and args >= 6) - ): - selected[name] = {'name': name, 'address': hex(int(func['@ENTRY_POINT'], 16) - addr_base)} - return selected - except Exception: - pass - raise ValueError('Failed to extract functions from Ghidra') - - def enumerate_processes(self) -> dict: - """ - Lists processes running on the device, returning a mapping of process names to PIDs. - """ - # https://github.com/frida/frida/issues/1225#issuecomment-604181822 - # Iterate through lines starting from the second line (skipping header) - processes = {} - sp = subprocess.run(['adb', '-s', self.device.id, 'shell', 'ps'], capture_output=True) - for line in sp.stdout.decode('utf-8').splitlines()[1:]: - try: - line = line.split() # USER,PID,PPID,VSZ,RSS,WCHAN,ADDR,S,NAME - name = ' '.join(line[8:]).strip() - name = name if name.startswith('[') else Path(name).name - processes[name] = int(line[1]) - except Exception: - pass - - return processes - - def _prepare_vendor_api(self, force: bool = False) -> int: - """ - Determines the most compatible vendor API version based on device processes. - """ - if force: - self.logger.warning('Using default vendor due to force flag') - return self.sdk_api - - # Check if forcing is not enabled and enumerate processes - details: [int] = [] - processes = self.enumerate_processes() - for k, v in Vendor.SDK_VERSIONS.items(): - # https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792 - for name, pid in processes.items(): - if v[2] in name: - self.logger.debug('Analysing... (%s)', v[2]) - session: Session = self.device.attach(pid) - script: Script = session.create_script(self.script) - script.load() - if script.exports_sync.getlibrary(v[3]): - details.append(k) - session.detach() - break - - # If no compatible versions found - if details: - # Find the closest SDK version to the current one, preferring lower matches in case of a tie. - sdk_api = min(details, key=lambda x: abs(x - self.sdk_api)) - - # Adjust SDK version if it exceeds the maximum supported version - if sdk_api == Vendor.SDK_MAX and self.sdk_api > Vendor.SDK_MAX: - sdk_api = self.sdk_api - elif sdk_api != self.sdk_api: - self.logger.warning('Using non-default Widevine version for SDK %s', sdk_api) - - return sdk_api - - raise EnvironmentError('Unable to detect Widevine, see: https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#drm-info') - - def _process_message(self, message: dict, data: bytes) -> None: - """ - Handles messages received from the Frida script. - """ - logger = logging.getLogger('Script') - level = message.get('payload') - - if isinstance(level, int): - # Process logging messages from Frida script - logger.log(level=level, msg=data.decode('utf-8')) - if level in (logging.FATAL, logging.CRITICAL): - self.running = False - elif level == 'device_info': - if data: - self._extract_device_info(data) - else: - logger.critical('No data for device info, invalid argument position') - self.running = False - elif level == 'private_key': - self._extract_private_key(data) - - def _extract_private_key(self, data: bytes) -> None: - """ - Extracts and stores the private key from the provided data. - """ - key = RSA.import_key(data) - key_id = key.n - if key_id not in self.keys: - self.keys[key_id] = key - self.logger.debug('Retrieved key: \n\n%s\n', key.exportKey('PEM').decode('utf-8')) - - def _extract_device_info(self, data: bytes) -> None: - """ - Extracts device information and associated private keys, storing them to disk. - """ - # https://github.com/devine-dl/pywidevine - signed_message = SignedMessage() - signed_message.ParseFromString(data) - - license_request = LicenseRequest() - license_request.ParseFromString(signed_message.msg) - - client_id: ClientIdentification = license_request.client_id - - signed_drm_certificate = SignedDrmCertificate() - drm_certificate = DrmCertificate() - - signed_drm_certificate.ParseFromString(client_id.token) - drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate) - - public_key = drm_certificate.public_key - key = RSA.importKey(public_key) - key_id = key.n - - private_key = self.keys.get(key_id) - if private_key: - path = Path() / 'device' / sanitize(str(self.device.name)) / 'private_keys' / str(drm_certificate.system_id) / str(key_id)[:10] - # https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146958022 - path = sanitize(path) - path.mkdir(parents=True, exist_ok=True) - path_client_id = path / 'client_id.bin' - path_private_key = path / 'private_key.pem' - - path_client_id.write_bytes(data=client_id.SerializeToString()) - path_private_key.write_bytes(data=private_key.exportKey('PEM')) - - self.logger.info('Dumped client ID: %s', path_client_id) - self.logger.info('Dumped private key: %s', path_private_key) - - if self.wvd: - # https://github.com/devine-dl/pywidevine/blob/master/pywidevine/main.py#L211 - client_info = {} - for entry in client_id.client_info: - client_info[entry.name] = entry.value - - device = Device( - client_id=client_id.SerializeToString(), - private_key=private_key.exportKey('PEM'), - type_=DeviceTypes.ANDROID, - security_level=3, - flags=None - ) - wvd_bin = device.dumps() - - name = f"{client_info['company_name']} {client_info['model_name']}" - if client_info.get('widevine_cdm_version'): - name += f" {client_info['widevine_cdm_version']}" - name += f" {crc32(wvd_bin).to_bytes(4, 'big').hex()}" - - name = unidecode(name.strip().lower().replace(' ', '_')) - out_path = path / f'{name}_{device.system_id}_l{device.security_level}.wvd' - - out_path.write_bytes(data=wvd_bin) - self.logger.info('Created WVD: %s', out_path) - - self.running = False - else: - self.logger.warning('Failed to intercept the private key') - - def hook_process(self, pid: int) -> bool: - """ - Hooks into the specified process to intercept DRM keys. - """ - session: Session = self.device.attach(pid) - script: Script = session.create_script(self.script) - script.on('message', self._process_message) - script.load() - - library_info = script.exports_sync.getlibrary(self.vendor.library) - if library_info: - self.logger.info('Library: %s (%s)', library_info['name'], library_info['path']) - - # Check if Ghidra XML functions loaded - if self.sdk_api > 33: - if not self.functions: - raise AttributeError('For SDK API > 33, specifying "functions" is required, see: https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md') - elif self.functions: - self.logger.warning('The "functions" attribute is deprecated for SDK API < 34') - - return script.exports_sync.hooklibrary(library_info['name']) - return False diff --git a/extractor/constants.py b/extractor/constants.py deleted file mode 100644 index 152eb79..0000000 --- a/extractor/constants.py +++ /dev/null @@ -1,60 +0,0 @@ -from enum import Enum - - -class Native(Enum): - STDIO = { - 'fclose', 'fflush', 'fgetc', 'fgetpos', 'fgets', 'fopen', 'fprintf', 'fputc', 'fputs', 'fread', 'freopen', - 'fscanf', 'fseek', 'fsetpos', 'ftell', 'fwrite', 'getc', 'getchar', 'gets', 'perror', 'printf', 'putc', - 'putchar', 'puts', 'remove', 'rename', 'rewind', 'scanf', 'setbuf', 'setvbuf', 'sprintf', 'sscanf', 'tmpfile', - 'tmpnam', 'ungetc', 'vfprintf', 'vprintf', 'vsprintf', 'fileno', 'feof', 'ferror'} - STDLIB = { - 'abort', 'abs', 'atexit', 'atof', 'atoi', 'atol', 'bsearch', 'calloc', 'div', 'exit', 'free', 'getenv', 'labs', - 'ldiv', 'malloc', 'mblen', 'mbstowcs', 'mbtowc', 'qsort', 'rand', 'realloc', 'srand', 'strtod', 'strtol', - 'strtoul', 'system', 'wcstombs', 'wctomb'} - STRING = { - 'memchr', 'memcmp', 'memcpy', 'memmove', 'memset', 'strcat', 'strchr', 'strcmp', 'strcoll', 'strcpy', 'strcspn', - 'strerror', 'strlen', 'strncat', 'strncmp', 'strncpy', 'strpbrk', 'strrchr', 'strspn', 'strstr', 'strtok', - 'strxfrm', 'strncasecmp'} - MATH = { - 'acos', 'asin', 'atan', 'atan2', 'cos', 'cosh', 'exp', 'fabs', 'floor', 'fmod', 'frexp', 'ldexp', 'log', - 'log10', 'modf', 'pow', 'sin', 'sinh', 'sqrt', 'tan', 'tanh'} - CTYPE = { - 'isalnum', 'isalpha', 'iscntrl', 'isdigit', 'isgraph', 'islower', 'isprint', 'ispunct', 'isspace', 'isupper', - 'isxdigit', 'tolower', 'toupper'} - TIME = {'asctime', 'clock', 'ctime', 'difftime', 'gmtime', 'localtime', 'mktime', 'strftime', 'time'} - UNISTD = { - 'access', 'alarm', 'chdir', 'chown', 'close', 'dup', 'dup2', 'execle', 'execv', 'execve', 'execvp', 'fork', - 'fpathconf', 'getcwd', 'getegid', 'geteuid', 'getgid', 'getgroups', 'getlogin', 'getopt', 'getpgid', 'getpgrp', - 'getpid', 'getppid', 'getuid', 'isatty', 'lseek', 'pathconf', 'pause', 'pipe', 'read', 'rmdir', 'setgid', - 'setpgid', 'setsid', 'setuid', 'sleep', 'sysconf', 'tcgetpgrp', 'tcsetpgrp', 'ttyname', 'ttyname_r', 'write', - 'fsync', 'unlink', 'syscall', 'getpagesize'} - FCNTL = {'creat', 'fcntl', 'open'} - SYS_TYPE = {'fd_set', 'FD_CLR', 'FD_ISSET', 'FD_SET', 'FD_ZERO'} - SYS_STAT = {'chmod', 'fchmod', 'fstat', 'mkdir', 'mkfifo', 'stat', 'umask'} - SYS_TIME = {'gettimeofday', 'select', 'settimeofday'} - SIGNAL = { - 'signal', 'raise', 'kill', 'sigaction', 'sigaddset', 'sigdelset', 'sigemptyset', 'sigfillset', 'sigismember', - 'sigpending', 'sigprocmask', 'sigsuspend', 'alarm', 'pause'} - SETJMP = {'longjmp', 'setjmp'} - ERRNO = {'errno', 'strerror', 'perror'} - ASSERT = {'assert'} - LOCAL = {'localeconv', 'setlocale'} - WCHAR = { - 'btowc', 'fgetwc', 'fgetws', 'fputwc', 'fputws', 'fwide', 'fwprintf', 'fwscanf', 'getwc', 'getwchar', 'mbrlen', - 'mbrtowc', 'mbsinit', 'mbsrtowcs', 'putwc', 'putwchar', 'swprintf', 'swscanf', 'ungetwc', 'vfwprintf', - 'vfwscanf', - 'vwprintf', 'vwscanf', 'wcrtomb', 'wcscat', 'wcschr', 'wcscmp', 'wcscoll', 'wcscpy', 'wcscspn', 'wcsftime', - 'wcslen', 'wcsncat', 'wcsncmp', 'wcsncpy', 'wcspbrk', 'wcsrchr', 'wcsrtombs', 'wcsspn', 'wcsstr', 'wcstod', - 'wcstok', 'wcstol', 'wcstombs', 'wcstoul', 'wcsxfrm', 'wctob', 'wmemchr', 'wmemcmp', 'wmemcpy', 'wmemmove', - 'wmemset', 'wprintf', 'wscanf'} - WCTYPE = { - 'iswalnum', 'iswalpha', 'iswcntrl', 'iswdigit', 'iswgraph', 'iswlower', 'iswprint', 'iswpunct', 'iswspace', - 'iswupper', 'iswxdigit', 'towlower', 'towupper', 'iswctype', 'wctype'} - STDDEF = {'NULL', 'offsetof', 'ptrdiff_t', 'size_t', 'wchar_t'} - STDARG = {'va_arg', 'va_end', 'va_start'} - DLFCN = {'dlclose', 'dlerror', 'dlopen', 'dlsym'} - DIRENT = {'closedir', 'opendir', 'readdir'} - SYS_SENDFILE = {'sendfile'} - SYS_MMAN = {'mmap', 'mprotect', 'munmap'} - SYS_UTSNAME = {'uname'} - LINK = {'dladdr'} diff --git a/extractor/keydive.js b/extractor/keydive.js deleted file mode 100644 index 732cc1e..0000000 --- a/extractor/keydive.js +++ /dev/null @@ -1,218 +0,0 @@ -/** - * KeyDive: Widevine L3 Extractor for Android Devices - * Enhances DRM key extraction for research and educational purposes. - * Source: https://github.com/hyugogirubato/KeyDive - */ - -// Placeholder values dynamically replaced at runtime. -const SDK_API = parseInt('${SDK_API}', 10); -const OEM_CRYPTO_API = JSON.parse('${OEM_CRYPTO_API}'); -const NATIVE_C_API = JSON.parse('${NATIVE_C_API}'); -const SYMBOLS = JSON.parse('${SYMBOLS}'); - - -// Logging levels to synchronize with Python's logging module. -const Level = { - NOTSET: 0, - DEBUG: 10, - INFO: 20, - // WARN: WARNING, - WARNING: 30, - ERROR: 40, - // FATAL: CRITICAL, - CRITICAL: 50 -}; - -// Utility for encoding strings into byte arrays. -// https://gist.github.com/Yaffle/5458286#file-textencodertextdecoder-js -function TextEncoder() {} -TextEncoder.prototype.encode = function (string) { - let octets = []; - let i = 0; - while (i < string.length) { - let codePoint = string.codePointAt(i); - let c = 0; - let bits = 0; - if (codePoint <= 0x007F) { - c = 0; - bits = 0x00; - } else if (codePoint <= 0x07FF) { - c = 6; - bits = 0xC0; - } else if (codePoint <= 0xFFFF) { - c = 12; - bits = 0xE0; - } else if (codePoint <= 0x1FFFFF) { - c = 18; - bits = 0xF0; - } - octets.push(bits | (codePoint >> c)); - while (c >= 6) { - c -= 6; - octets.push(0x80 | ((codePoint >> c) & 0x3F)); - } - i += codePoint >= 0x10000 ? 2 : 1; - } - return octets; -}; - -const print = (level, message) => { - message = typeof message === 'object' ? JSON.stringify(message) : message; - send(level, new TextEncoder().encode(message)); -} - -// Identifies and returns the specified library. -const getLibrary = (name) => { - try { - return Process.getModuleByName(name); - } catch (e) { - return undefined; - } -} - -// Hooks into specified functions within a library, aiming to extract keys and disable privacy mode. -const hookLibrary = (name) => { - // https://github.com/poxyran/misc/blob/master/frida-enumerate-imports.py - const library = getLibrary(name); - if (!library) return false; - - let functions, target; - if (SYMBOLS.length > 0) { - functions = SYMBOLS.map(symbol => ({ - type: 'function', - name: symbol.name, - address: ptr(parseInt(symbol.address, 16) + parseInt(library.base, 16)) - })); - } else { - functions = library.enumerateExports(); - // functions = [...library.enumerateExports(), ...library.enumerateImports()]; - target = functions.find(func => OEM_CRYPTO_API.includes(func.name)); - } - - // Remove native C functions - functions = functions.filter(func => !NATIVE_C_API.includes(func.name)); - - let hookedCount = 0; - functions.forEach((func) => { - if (func.type !== 'function') return; - - const funcName = func.name; - const funcAddr = func.address; - - try { - let funcHooked = true; - if (funcName.includes('UsePrivacyMode')) { - disablePrivacyMode(funcAddr); - } else if (funcName.includes('PrepareKeyRequest')) { - prepareKeyRequest(funcAddr); - } else if (target === func || (!target && funcName.match(/^[a-z]+$/))) { - getPrivateKey(funcAddr); - } else { - funcHooked = false; - } - - if (funcHooked) { - hookedCount++; - print(Level.DEBUG, `Hooked (${funcAddr}): ${funcName}`); - } - } catch (e) { - print(Level.ERROR, `${e.message} for ${funcName}`); - } - }); - - if (hookedCount < 3) { - print(Level.ERROR, 'Insufficient functions hooked'); - return false; - } - return true; -} - -const disablePrivacyMode = (address) => { - Interceptor.attach(ptr(address), { - onLeave: function (retval) { - retval.replace(ptr(0)); - } - }); -} - -const prepareKeyRequest = (address) => { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - let index; - if ([23, 31, 32, 33].includes(SDK_API)) { - index = 5; - } else if ([24, 25, 26, 27, 28, 29, 30].includes(SDK_API)) { - index = 4; - } else if ([34].includes(SDK_API)) { - index = 5; // Possibly value 5 too, depending on the architecture - } else { - index = 5; // Default index assignment - print(Level.WARNING, 'SDK API not implemented'); - print(Level.WARNING, `Defaulting to args[${index}] for PrepareKeyRequest`); - } - this.ret = args[index]; - }, - onLeave: function () { - if (this.ret) { - const size = Memory.readU32(ptr(this.ret).add(Process.pointerSize)); - const data = Memory.readByteArray(this.ret.add(Process.pointerSize * 2).readPointer(), size); - send('device_info', data); - } - } - }); -} - -const getPrivateKey = (address) => { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - if (!args[6].isNull()) { - const size = args[6].toInt32(); - if (size >= 1000 && size <= 2000 && !args[5].isNull()) { - const buffer = args[5].readByteArray(size); - const bytes = new Uint8Array(buffer); - // Check for DER encoding markers for the beginning of a private key (MII). - if (bytes[0] === 0x30 && bytes[1] === 0x82) { - try { - // Attempt to extract and send the private key. - const binaryString = a2bs(bytes); - const keyLength = getKeyLength(binaryString); // ASN.1 DER - const key = bytes.slice(0, keyLength); - print(Level.DEBUG, `Function getPrivateKey() at ${address}`); - send('private_key', key); - } catch (e) { - print(Level.ERROR, `${e.message} (${address})`); - } - } - } - } - } - }); -} - -const a2bs = (bytes) => Array.from(bytes).map(byte => String.fromCharCode(byte)).join(''); - -const getKeyLength = (key) => { - let pos = 1; // Skip the initial tag - // Extract length byte, ignoring the long-form indicator bit - let lengthByte = key.charCodeAt(pos++) & 0x7F; - // If lengthByte indicates a short form, return early. - /* - if (lengthByte < 0x80) { - return pos + lengthByte; - } - */ - - // For long-form, calculate the length value. - let lengthValue = 0; - for (let i = 0; i < lengthByte; i++) { - lengthValue = (lengthValue << 8) + key.charCodeAt(pos++); - } - return pos + Math.abs(lengthValue); -} - - -// Exposing functions for RPC calls. -rpc.exports = { - getlibrary: getLibrary, - hooklibrary: hookLibrary -}; \ No newline at end of file diff --git a/extractor/uils.py b/extractor/uils.py deleted file mode 100644 index eb2acda..0000000 --- a/extractor/uils.py +++ /dev/null @@ -1,23 +0,0 @@ -import re -from typing import Union - -from pathlib import Path - - -def sanitize(path: Union[Path, str]) -> Path: - if isinstance(path, str): - path = Path(path) - paths = [path.name, *[p.name for p in path.parents if p.name]][::-1] - for i, p in enumerate(paths): - p = p.replace('...', '').strip() - p = re.sub(r'[<>:"/|?*\x00-\x1F]', '_', p) - paths[i] = p - - return Path().joinpath(*paths) - - -if __name__ == '__main__': - path = Path() / 'hello rgtgr/sdg' - print(path) - path = sanitize(path) - print(path) diff --git a/extractor/vendor.py b/extractor/vendor.py deleted file mode 100644 index ddd653f..0000000 --- a/extractor/vendor.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import logging - -logger = logging.getLogger('Vendor') - - -class Vendor: - """ - Represents Widevine DRM Vendor details for different Android SDK versions. - """ - # https://developer.android.com/tools/releases/platforms - SDK_VERSIONS = { - # 34: (18, '18.0.0', 'android.hardware.drm-service.widevine-v17', 'android.hardware.drm-service.widevine-v17'), - 34: (18, '18.0.0', 'android.hardware.drm-service.widevine', 'android.hardware.drm-service.widevine'), - 33: (17, '17.0.0', 'android.hardware.drm-service.widevine', 'libwvaidl.so'), - 32: (16, '16.1.0', 'android.hardware.drm@1.4-service.widevine', 'libwvhidl.so'), - 31: (16, '16.1.0', 'android.hardware.drm@1.4-service.widevine', 'libwvhidl.so'), - 30: (16, '16.0.0', 'android.hardware.drm@1.3-service.widevine', 'libwvhidl.so'), - 29: (15, '15.0.0', 'android.hardware.drm@1.2-service.widevine', 'libwvhidl.so'), - 28: (14, '14.0.0', 'android.hardware.drm@1.1-service.widevine', 'libwvhidl.so'), - 27: (13, '5.1.0', 'android.hardware.drm@1.0-service.widevine', 'libwvhidl.so'), - 26: (13, '1.0', 'android.hardware.drm@1.0-service.widevine', 'libwvhidl.so'), - 25: (11, '1.0', 'mediadrmserver', 'libwvdrmengine.so'), - 24: (11, '1.0', 'mediadrmserver', 'libwvdrmengine.so'), - 23: (11, '1.0', 'mediaserver', 'libwvdrmengine.so') - } - SDK_MAX = max(SDK_VERSIONS.keys()) - - def __init__(self, oem: int, version: str, process: str, library: str): - """ - Initialize a Vendor instance. - - :param oem: OEM Crypto API level. - :param version: Widevine CDM version. - :param process: The process name associated with the Widevine DRM. - :param library: The library file name used by the DRM process. - """ - self.oem = oem - self.version = version - self.process = process - self.library = library - - @classmethod - def from_sdk_api(cls, sdk_api: int) -> Vendor: - """ - Creates a Vendor instance based on the Android SDK API level. - - :param sdk_api: Android SDK API level. - :return: A Vendor instance with DRM details. - """ - assert sdk_api > 22, 'Widevine not implemented for SDK <= 22' - - vendor_details = cls.SDK_VERSIONS.get(sdk_api) - if not vendor_details: - vendor_details = cls.SDK_VERSIONS[cls.SDK_MAX] - logger.warning('CMD version is not yet implemented') - logger.warning('Using closest supported CDM version: %s', vendor_details[1]) - else: - logger.info('CDM version: %s' % vendor_details[1]) - logger.info('OEM Crypto API: %s' % vendor_details[0]) - return cls(*vendor_details) diff --git a/keydive.py b/keydive.py deleted file mode 100644 index 3df3ae6..0000000 --- a/keydive.py +++ /dev/null @@ -1,65 +0,0 @@ -import argparse -import logging -import subprocess -import time - -import coloredlogs -from pathlib import Path - -import extractor -from extractor.cdm import Cdm - -coloredlogs.install( - fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.DEBUG) - -if __name__ == '__main__': - logger = logging.getLogger('KeyDive') - - # Parse command line arguments for device ID - parser = argparse.ArgumentParser(description='Extract Widevine L3 keys from an Android device.') - parser.add_argument('-a', '--auto', required=False, action='store_true', help='Open Bitmovin\'s demo automatically.') - parser.add_argument('-d', '--device', required=False, type=str, help='Target Android device ID.') - parser.add_argument('-f', '--functions', required=False, type=Path, help='Path to Ghidra XML functions file.') - parser.add_argument('-w', '--wvd', required=False, action='store_true', help='Generate WVD.') - parser.add_argument('--force', required=False, action='store_true', help='Force using the default vendor (skipping analysis).') - - args = parser.parse_args() - - try: - logger.info('Version: %s', extractor.__version__) - - # Ensure the ADB server is running - sp = subprocess.run(['adb', 'start-server'], capture_output=True) - if sp.returncode != 0: - raise EnvironmentError('ADB is not recognized as an environment variable, see https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge') - - # Initialize the CDM handler with the specified or default device - cdm = Cdm(device=args.device, functions=args.functions, force=args.force, wvd=args.wvd) - - # Attempt to locate and identify the Widevine process on the target device - pid = cdm.enumerate_processes().get(cdm.vendor.process) - if not pid: - raise EnvironmentError('Widevine process not found on the device') - logger.info('Process: %s (%s)', pid, cdm.vendor.process) - - # Hook into the identified process for DRM key extraction - if not cdm.hook_process(pid=pid): - raise Exception('Failed to hook into the Widevine process') - logger.info('Successfully hooked. To test, play a DRM-protected video: https://bitmovin.com/demos/drm') - - if args.auto: - logger.info('Starting DRM player launch process...') - sp = subprocess.run(['adb', '-s', cdm.device.id, 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', 'https://bitmovin.com/demos/drm'], capture_output=True) - if sp.returncode != 0: - logger.error('Error launching DRM player: %s' % sp.stdout.decode('utf-8').strip()) - - # Keep script running while extracting keys - while cdm.running: - time.sleep(1) - except KeyboardInterrupt: - pass - except Exception as e: - logger.critical(e) - logger.info('Exiting') diff --git a/keydive/__init__.py b/keydive/__init__.py new file mode 100644 index 0000000..4d4781f --- /dev/null +++ b/keydive/__init__.py @@ -0,0 +1,5 @@ +from .core import Core +from .cdm import Cdm +from .vendor import Vendor + +__version__ = '2.0.0' diff --git a/keydive/__main__.py b/keydive/__main__.py new file mode 100644 index 0000000..62f137a --- /dev/null +++ b/keydive/__main__.py @@ -0,0 +1,171 @@ +import argparse +import logging +import subprocess +import time +from datetime import datetime +from pathlib import Path + +import coloredlogs + +import keydive +from keydive.cdm import Cdm +from keydive.constants import CDM_VENDOR_API +from keydive.core import Core + + +def configure_logging(path: Path, verbose: bool) -> Path: + """ + Configures logging for the application. + + Args: + path (Path, optional): The path for log files. + verbose (bool): Whether to enable verbose logging. + + Returns: + Path: The path of log file. + """ + # Get the root logger + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG if verbose else logging.INFO) + + # Clear any existing handlers (optional, to avoid duplicate logs if reconfiguring) + if root_logger.hasHandlers(): + root_logger.handlers.clear() + + file_path = None + if path: + if path.is_file(): + path = path.parent + + path.mkdir(parents=True, exist_ok=True) + + # Create a file handler + file_path = path / ('keydive_%s.log' % datetime.now().strftime('%Y-%m-%d_%H-%M-%S')) + file_path = file_path.resolve(strict=False) + file_handler = logging.FileHandler(file_path) + file_handler.setLevel(logging.DEBUG) + + formatter = logging.Formatter( + fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + file_handler.setFormatter(formatter) + + # Add the file handler to the root logger + root_logger.addHandler(file_handler) + + # Configure coloredlogs for console output + coloredlogs.install( + fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + level=logging.DEBUG if verbose else logging.INFO, + logger=root_logger + ) + return file_path + + +def main() -> None: + parser = argparse.ArgumentParser(description='Extract Widevine L3 keys from an Android device.') + + # Global options + opt_global = parser.add_argument_group('Global options') + opt_global.add_argument('-d', '--device', required=False, type=str, metavar='', help='Specify the target Android device ID to connect with via ADB.') + opt_global.add_argument('-v', '--verbose', required=False, action='store_true', help='Enable verbose logging for detailed debug output.') + opt_global.add_argument('-l', '--log', required=False, type=Path, metavar='', help='Directory to store log files.') + opt_global.add_argument('--delay', required=False, type=float, metavar='', default=1, help='Delay (in seconds) between process checks in the watcher.') + opt_global.add_argument('--version', required=False, action='store_true', help='Display KeyDive version information.') + + # Cdm options + opt_cdm = parser.add_argument_group('Cdm options') + opt_cdm.add_argument('-a', '--auto', required=False, action='store_true', help='Automatically open Bitmovin\'s demo.') + opt_cdm.add_argument('-c', '--challenge', required=False, type=Path, metavar='', help='Path to unencrypted challenge for extracting client ID.') + opt_cdm.add_argument('-w', '--wvd', required=False, action='store_true', help='Generate WVD.') + opt_cdm.add_argument('-o', '--output', required=False, type=Path, default=Path('device'), metavar='', help='Output directory path for extracted data.') + opt_cdm.add_argument('-f', '--functions', required=False, type=Path, metavar='', help='Path to Ghidra XML functions file.') + args = parser.parse_args() + + if args.version: + print(f'KeyDive {keydive.__version__}') + exit(0) + + # Configure logging + log_path = configure_logging(path=args.log, verbose=args.verbose) + logger = logging.getLogger('KeyDive') + logger.info('Version: %s', keydive.__version__) + + try: + # Start the ADB server if not already running + sp = subprocess.run(['adb', 'start-server'], capture_output=True) + if sp.returncode != 0: + raise EnvironmentError('ADB is not recognized as an environment variable, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge') + + # Initialize Cdm instance + cdm = Cdm() + if args.challenge: + cdm.set_challenge(data=args.challenge) + + # Initialize Core instance for interacting with the device + core = Core(cdm=cdm, device=args.device, symbols=args.functions) + + # Process watcher loop + logger.info('Watcher delay: %ss' % args.delay) + current = None + while core.running: + # https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792 + processes = { + key: (name, pid) + for name, pid in core.enumerate_processes().items() + for key in CDM_VENDOR_API.keys() if key in name + } + + if not processes: + raise EnvironmentError('Unable to detect Widevine, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#drm-info') + + # Check if the current process has changed + if current: + if current not in [v[1] for v in processes.values()]: + logger.warning('Widevine process has changed') + current = None + elif cdm.export(args.output, args.wvd): + raise KeyboardInterrupt + + # If current process not found, attempt to hook into the detected processes + if not current: + logger.debug('Analysing...') + + for key, (name, pid) in processes.items(): + if current: + break + for vendor in CDM_VENDOR_API[key]: + if core.hook_process(pid=pid, vendor=vendor): + logger.info('Process: %s (%s)', pid, name) + current = pid + break + elif not core.running: + raise KeyboardInterrupt + + if current: + logger.info('Successfully hooked.') + if args.auto: + logger.info('Starting DRM player launch process...') + sp = subprocess.run(['adb', '-s', str(core.device.id), 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', 'https://bitmovin.com/demos/drm'], capture_output=True) + if sp.returncode != 0: + logger.error('Error launching DRM player: %s' % sp.stdout.decode('utf-8').strip()) + else: + logger.warning('Widevine library not found, searching...') + + # Delay before next iteration + time.sleep(args.delay) + except KeyboardInterrupt: + pass + except Exception as e: + logger.critical(e, exc_info=args.verbose) + + # Final logging and exit + if log_path: + logger.info('Log file: %s' % log_path) + logger.info('Exiting') + + +if __name__ == '__main__': + main() diff --git a/keydive/cdm.py b/keydive/cdm.py new file mode 100644 index 0000000..cd8f84d --- /dev/null +++ b/keydive/cdm.py @@ -0,0 +1,177 @@ +import json +import logging +import re +from pathlib import Path +from typing import Union +from zlib import crc32 + +from Cryptodome.PublicKey import RSA +from pywidevine import Device +from pywidevine.device import DeviceTypes +from pywidevine.license_protocol_pb2 import SignedMessage, LicenseRequest, ClientIdentification, SignedDrmCertificate, DrmCertificate +from unidecode import unidecode + + +def sanitize(path: Path) -> Path: + """ + Sanitizes the given path by replacing invalid characters. + + Args: + path (Path): The path to sanitize. + + Returns: + Path: The sanitized path. + """ + paths = [path.name, *[p.name for p in path.parents if p.name]][::-1] + for i, p in enumerate(paths): + p = p.replace('...', '').strip() + p = re.sub(r'[<>:"/|?*\x00-\x1F]', '_', p) + paths[i] = p + + return Path().joinpath(*paths) + + +class Cdm: + """ + The Cdm class manages CDM-related operations, such as setting challenge data, + extracting and storing private keys, and exporting device information. + """ + + def __init__(self): + self.logger = logging.getLogger(self.__class__.__name__) + # https://github.com/devine-dl/pywidevine + self.client_id: dict[int, ClientIdentification] = {} + self.private_key: dict[int, RSA] = {} + + def __client_info(self, client_id: ClientIdentification) -> dict: + """ + Converts client identification information to a dictionary. + + Args: + client_id (ClientIdentification): The client identification. + + Returns: + dict: A dictionary of client information. + """ + return {e.name: e.value for e in client_id.client_info} + + def set_challenge(self, data: Union[Path, bytes]) -> None: + """ + Sets the challenge data by extracting device information. + + Args: + data (Union[Path, bytes]): The challenge data as a file path or bytes. + """ + try: + if isinstance(data, Path): + if not data.is_file(): + raise FileNotFoundError(data) + data = data.read_bytes() + + signed_message = SignedMessage() + signed_message.ParseFromString(data) + + license_request = LicenseRequest() + license_request.ParseFromString(signed_message.msg) + + client_id: ClientIdentification = license_request.client_id + self.set_client_id(data=client_id.SerializeToString()) + except Exception as e: + self.logger.error('Error parsing challenge: %s', e) + + def set_private_key(self, data: bytes) -> None: + """ + Sets the private key from the provided data. + + Args: + data (bytes): The private key data. + """ + try: + key = RSA.import_key(data) + if key.n not in self.private_key: + self.logger.debug('Receive private key: \n\n%s\n', key.exportKey('PEM').decode('utf-8')) + self.private_key[key.n] = key + except Exception as e: + self.logger.error('Error parsing private key: %s', e) + + def set_client_id(self, data: Union[ClientIdentification, bytes]) -> None: + """ + Sets the client ID from the provided data. + + Args: + data (Union[ClientIdentification, bytes]): The client ID data. + """ + try: + if isinstance(data, ClientIdentification): + client_id = data + else: + client_id = ClientIdentification() + client_id.ParseFromString(data) + + signed_drm_certificate = SignedDrmCertificate() + drm_certificate = DrmCertificate() + + signed_drm_certificate.ParseFromString(client_id.token) + drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate) + + public_key = drm_certificate.public_key + key = RSA.importKey(public_key) + + if key.n not in self.client_id: + self.logger.debug('Receive client id: \n\n%s\n', json.dumps(self.__client_info(client_id), indent=2)) + self.client_id[key.n] = client_id + except Exception as e: + self.logger.error('Error parsing client ID: %s', e) + + def export(self, parent: Path, wvd: bool = False) -> bool: + """ + Exports the client ID and private key to disk. + + Args: + parent (Path): The parent directory to export the files to. + wvd (bool): Whether to export WVD files. + + Returns: + bool: True if any keys were exported, otherwise False. + """ + keys = set(self.client_id.keys()) & set(self.private_key.keys()) + for k in keys: + client_info = self.__client_info(self.client_id[k]) + # https://github.com/devine-dl/pywidevine/blob/master/pywidevine/main.py#L211 + device = Device( + client_id=self.client_id[k].SerializeToString(), + private_key=self.private_key[k].exportKey('PEM'), + type_=DeviceTypes.ANDROID, + security_level=3, + flags=None + ) + + # https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146958022 + parent = sanitize(parent / client_info['company_name'] / client_info['model_name'] / str(device.system_id) / str(k)[:10]) + parent.mkdir(parents=True, exist_ok=True) + + path_id_bin = parent / 'client_id.bin' + path_id_bin.write_bytes(data=device.client_id.SerializeToString()) + self.logger.info('Exported client ID: %s', path_id_bin) + + path_key_bin = parent / 'private_key.pem' + path_key_bin.write_bytes(data=device.private_key.exportKey('PEM')) + self.logger.info('Exported private key: %s', path_key_bin) + + if wvd: + wvd_bin = device.dumps() + + name = f"{client_info['company_name']} {client_info['model_name']}" + if client_info.get('widevine_cdm_version'): + name += f" {client_info['widevine_cdm_version']}" + name += f" {crc32(wvd_bin).to_bytes(4, 'big').hex()}" + name = unidecode(name.strip().lower().replace(' ', '_')) + path_wvd = parent / f'{name}_{device.system_id}_l{device.security_level}.wvd' + + path_wvd.write_bytes(data=wvd_bin) + self.logger.info('Exported WVD: %s', path_wvd) + + return len(keys) > 0 + + +__all__ = ('Cdm',) diff --git a/keydive/constants.py b/keydive/constants.py new file mode 100644 index 0000000..84ea14d --- /dev/null +++ b/keydive/constants.py @@ -0,0 +1,119 @@ +from keydive.vendor import Vendor + +NATIVE_C_API = { + # STDIO + 'fclose', 'fflush', 'fgetc', 'fgetpos', 'fgets', 'fopen', 'fprintf', 'fputc', 'fputs', 'fread', 'freopen', + 'fscanf', 'fseek', 'fsetpos', 'ftell', 'fwrite', 'getc', 'getchar', 'gets', 'perror', 'printf', 'putc', + 'putchar', 'puts', 'remove', 'rename', 'rewind', 'scanf', 'setbuf', 'setvbuf', 'sprintf', 'sscanf', 'tmpfile', + 'tmpnam', 'ungetc', 'vfprintf', 'vprintf', 'vsprintf', 'fileno', 'feof', 'ferror', + # STDLIB + 'abort', 'abs', 'atexit', 'atof', 'atoi', 'atol', 'bsearch', 'calloc', 'div', 'exit', 'free', 'getenv', 'labs', + 'ldiv', 'malloc', 'mblen', 'mbstowcs', 'mbtowc', 'qsort', 'rand', 'realloc', 'srand', 'strtod', 'strtol', + 'strtoul', 'system', 'wcstombs', 'wctomb', + # STRING + 'memchr', 'memcmp', 'memcpy', 'memmove', 'memset', 'strcat', 'strchr', 'strcmp', 'strcoll', 'strcpy', 'strcspn', + 'strerror', 'strlen', 'strncat', 'strncmp', 'strncpy', 'strpbrk', 'strrchr', 'strspn', 'strstr', 'strtok', + 'strxfrm', 'strncasecmp', + # MATH + 'acos', 'asin', 'atan', 'atan2', 'cos', 'cosh', 'exp', 'fabs', 'floor', 'fmod', 'frexp', 'ldexp', 'log', + 'log10', 'modf', 'pow', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', + # CTYPE + 'isalnum', 'isalpha', 'iscntrl', 'isdigit', 'isgraph', 'islower', 'isprint', 'ispunct', 'isspace', 'isupper', + 'isxdigit', 'tolower', 'toupper', + # TIME + 'asctime', 'clock', 'ctime', 'difftime', 'gmtime', 'localtime', 'mktime', 'strftime', 'time', + # UNISTD + 'access', 'alarm', 'chdir', 'chown', 'close', 'dup', 'dup2', 'execle', 'execv', 'execve', 'execvp', 'fork', + 'fpathconf', 'getcwd', 'getegid', 'geteuid', 'getgid', 'getgroups', 'getlogin', 'getopt', 'getpgid', 'getpgrp', + 'getpid', 'getppid', 'getuid', 'isatty', 'lseek', 'pathconf', 'pause', 'pipe', 'read', 'rmdir', 'setgid', + 'setpgid', 'setsid', 'setuid', 'sleep', 'sysconf', 'tcgetpgrp', 'tcsetpgrp', 'ttyname', 'ttyname_r', 'write', + 'fsync', 'unlink', 'syscall', 'getpagesize', + # FCNTL + 'creat', 'fcntl', 'open', + # SYS_TYPE + 'fd_set', 'FD_CLR', 'FD_ISSET', 'FD_SET', 'FD_ZERO', + # SYS_STAT + 'chmod', 'fchmod', 'fstat', 'mkdir', 'mkfifo', 'stat', 'umask', + # SYS_TIME + 'gettimeofday', 'select', 'settimeofday', + # SIGNAL + 'signal', 'raise', 'kill', 'sigaction', 'sigaddset', 'sigdelset', 'sigemptyset', 'sigfillset', 'sigismember', + 'sigpending', 'sigprocmask', 'sigsuspend', 'alarm', 'pause', + # SETJMP + 'longjmp', 'setjmp', + # ERRNO + 'errno', 'strerror', 'perror', + # ASSERT + 'assert', + # LOCAL + 'localeconv', 'setlocale', + # WCHAR + 'btowc', 'fgetwc', 'fgetws', 'fputwc', 'fputws', 'fwide', 'fwprintf', 'fwscanf', 'getwc', 'getwchar', 'mbrlen', + 'mbrtowc', 'mbsinit', 'mbsrtowcs', 'putwc', 'putwchar', 'swprintf', 'swscanf', 'ungetwc', 'vfwprintf', + 'vfwscanf', 'vwprintf', 'vwscanf', 'wcrtomb', 'wcscat', 'wcschr', 'wcscmp', 'wcscoll', 'wcscpy', 'wcscspn', + 'wcsftime', 'wcslen', 'wcsncat', 'wcsncmp', 'wcsncpy', 'wcspbrk', 'wcsrchr', 'wcsrtombs', 'wcsspn', 'wcsstr', + 'wcstod', 'wcstok', 'wcstol', 'wcstombs', 'wcstoul', 'wcsxfrm', 'wctob', 'wmemchr', 'wmemcmp', 'wmemcpy', + 'wmemmove', 'wmemset', 'wprintf', 'wscanf', + # WCTYPE + 'iswalnum', 'iswalpha', 'iswcntrl', 'iswdigit', 'iswgraph', 'iswlower', 'iswprint', 'iswpunct', 'iswspace', + 'iswupper', 'iswxdigit', 'towlower', 'towupper', 'iswctype', 'wctype', + # STDDEF + 'NULL', 'offsetof', 'ptrdiff_t', 'size_t', 'wchar_t', + # STDARG + 'va_arg', 'va_end', 'va_start', + # DLFCN + 'dlclose', 'dlerror', 'dlopen', 'dlsym', + # DIRENT + 'closedir', 'opendir', 'readdir', + # SYS_SENDFILE + 'sendfile', + # SYS_MMAN + 'mmap', 'mprotect', 'munmap', + # SYS_UTSNAME + 'uname', + # LINK + 'dladdr' +} + +OEM_CRYPTO_API = { + # Mapping of function names across different API levels (obfuscated names may vary). + 'rnmsglvj', 'polorucp', 'kqzqahjq', 'pldrclfq', 'kgaitijd', 'cwkfcplc', 'crhqcdet', 'ulns', 'dnvffnze', 'ygjiljer', + 'qbjxtubz', 'qkfrcjtw', 'rbhjspoh', 'zgtjmxko', 'igrqajte', 'ofskesua', 'qllcoacg', 'pukctkiv', 'ehmduqyt' + # Add more as needed for different versions. +} + +CDM_VENDOR_API = { + 'mediaserver': { + Vendor(11, '1.0', 'libwvdrmengine.so') + }, + 'mediadrmserver': { + Vendor(11, '1.0', 'libwvdrmengine.so') + }, + 'android.hardware.drm@1.0-service.widevine': { + Vendor(13, '5.1.0', 'libwvhidl.so') + }, + 'android.hardware.drm@1.1-service.widevine': { + Vendor(14, '14.0.0', 'libwvhidl.so') + }, + 'android.hardware.drm@1.2-service.widevine': { + Vendor(15, '15.0.0', 'libwvhidl.so') + }, + 'android.hardware.drm@1.3-service.widevine': { + Vendor(16, '16.0.0', 'libwvhidl.so') + }, + 'android.hardware.drm@1.4-service.widevine': { + Vendor(16, '16.1.0', 'libwvhidl.so') + }, + 'android.hardware.drm-service.widevine': { + Vendor(17, '17.0.0', 'libwvaidl.so'), + Vendor(18, '18.0.0', 'android.hardware.drm-service.widevine') + } +} + +# https://developer.android.com/tools/releases/platforms +CDM_FUNCTION_API = { + 'UsePrivacyMode', + 'GetCdmClientPropertySet', + 'PrepareKeyRequest', + 'getOemcryptoDeviceId' +} diff --git a/keydive/core.py b/keydive/core.py new file mode 100644 index 0000000..1ba30c4 --- /dev/null +++ b/keydive/core.py @@ -0,0 +1,235 @@ +import json +import logging +import re +import subprocess +from pathlib import Path + +import frida +import xmltodict +from frida.core import Device, Session, Script + +from keydive.cdm import Cdm +from keydive.constants import OEM_CRYPTO_API, NATIVE_C_API, CDM_FUNCTION_API +from keydive.vendor import Vendor + + +class Core: + """ + Core class for handling DRM operations and device interactions. + """ + + def __init__(self, cdm: Cdm, device: str = None, symbols: Path = None): + """ + Initializes a Core instance. + + Args: + cdm (Cdm): Instance of Cdm for managing DRM related operations. + device (str, optional): ID of the Android device to connect to via ADB. Defaults to None (uses USB device). + symbols (Path, optional): Path to Ghidra XML functions file for symbol extraction. Defaults to None. + """ + self.logger = logging.getLogger(self.__class__.__name__) + self.running = True + self.cdm = cdm + + # Select device based on provided ID or default to the first USB device. + self.device: Device = frida.get_device(id=device, timeout=5) if device else frida.get_usb_device(timeout=5) + self.logger.info('Device: %s (%s)', self.device.name, self.device.id) + + # Obtain device properties + properties = self.device_properties() + self.logger.info('SDK API: %s', properties['ro.build.version.sdk']) + self.logger.info('ABI CPU: %s', properties['ro.product.cpu.abi']) + + # Load the hook script + self.symbols = self.__prepare_symbols(symbols) + self.script = self.__prepare_hook_script() + self.logger.info('Script loaded successfully') + + def __prepare_hook_script(self) -> str: + """ + Prepares the hook script content by injecting the library-specific scripts. + + Returns: + str: The prepared script content. + """ + content = Path(__file__).with_name('keydive.js').read_text() + + # Replace placeholders in script template + replacements = { + '${OEM_CRYPTO_API}': json.dumps(list(OEM_CRYPTO_API)), + '${NATIVE_C_API}': json.dumps(list(NATIVE_C_API)), + '${SYMBOLS}': json.dumps(self.symbols) + } + + for placeholder, value in replacements.items(): + content = content.replace(placeholder, value) + + return content + + def __prepare_symbols(self, path: Path) -> list: + """ + Parses the provided XML functions file to select relevant functions. + + Args: + path (Path): Path to Ghidra XML functions file. + + Returns: + list: List of selected functions as dictionaries. + + Raises: + FileNotFoundError: If the functions file is not found. + ValueError: If functions extraction fails. + """ + if not path: + return [] + elif not path.is_file(): + raise FileNotFoundError('Functions file not found') + + try: + program = xmltodict.parse(path.read_bytes())['PROGRAM'] + addr_base = int(program['@IMAGE_BASE'], 16) + functions = program['FUNCTIONS']['FUNCTION'] + + # Find a target function from a predefined list + target = next((f['@NAME'] for f in functions if f['@NAME'] in OEM_CRYPTO_API), None) + + # Extract relevant functions + selected = {} + for func in functions: + name = func['@NAME'] + args = len(func.get('REGISTER_VAR', [])) + + # Add function if it matches specific criteria + if name not in selected and ( + name == target + or any(keyword in name for keyword in CDM_FUNCTION_API) + or (not target and re.match(r'^[a-z]+$', name) and args >= 6) + ): + selected[name] = { + 'type': 'function', + 'name': name, + 'address': hex(int(func['@ENTRY_POINT'], 16) - addr_base) + } + return list(selected.values()) + except Exception as e: + raise ValueError('Failed to extract functions from Ghidra') from e + + def device_properties(self) -> dict: + """ + Retrieves system properties from the connected device using ADB shell commands. + + Returns: + dict: A dictionary of device properties. + """ + # https://source.android.com/docs/core/architecture/configuration/add-system-properties?#shell-commands + properties = {} + sp = subprocess.run(['adb', '-s', str(self.device.id), 'shell', 'getprop'], capture_output=True) + for line in sp.stdout.decode('utf-8').splitlines(): + match = re.match(r'\[(.*?)\]: \[(.*?)\]', line) + if match: + key, value = match.groups() + # Attempt to cast numeric and boolean values to appropriate types + try: + value = int(value) + except ValueError: + if value.lower() in ('true', 'false'): + value = value.lower() == 'true' + properties[key] = value + return properties + + def enumerate_processes(self) -> dict: + """ + Lists processes running on the device, returning a mapping of process names to PIDs. + + Returns: + dict: A dictionary mapping process names to PIDs. + """ + processes = {} + + # https://github.com/frida/frida/issues/1225#issuecomment-604181822 + prompt = ['adb', '-s', str(self.device.id), 'shell', 'ps'] + sp = subprocess.run([*prompt, '-A'], capture_output=True) + if sp.returncode != 0: + sp = subprocess.run(prompt, capture_output=True) + + # Iterate through lines starting from the second line (skipping header) + for line in sp.stdout.decode('utf-8').splitlines()[1:]: + try: + line = line.split() # USER,PID,PPID,VSZ,RSS,WCHAN,ADDR,S,NAME + name = ' '.join(line[8:]).strip() + name = name if name.startswith('[') else Path(name).name + processes[name] = int(line[1]) + except Exception: + pass + + return processes + + def __process_message(self, message: dict, data: bytes) -> None: + """ + Handles messages received from the Frida script. + + Args: + message (dict): The message payload. + data (bytes): The raw data associated with the message. + """ + logger = logging.getLogger('Script') + level = message.get('payload') + + if isinstance(level, int): + # Process logging messages from Frida script + logger.log(level=level, msg=data.decode('utf-8')) + if level in (logging.FATAL, logging.CRITICAL): + self.running = False + elif level == 'challenge': + self.cdm.set_challenge(data=data) + elif level == 'private_key': + self.cdm.set_private_key(data=data) + elif level == 'client_id': + self.cdm.set_client_id(data=data) + else: + self.logger.warning('Malformed message: %s -> %s' % (level, data)) + + def hook_process(self, pid: int, vendor: Vendor, timeout: int = 0) -> bool: + """ + Hooks into the specified process. + + Args: + pid (int): The process ID to hook. + vendor (Vendor): Instance of Vendor class representing the vendor information. + timeout (int, optional): Timeout for attaching to the process. Defaults to 0. + + Returns: + bool: True if the process was successfully hooked, otherwise False. + """ + try: + session: Session = self.device.attach(pid, persist_timeout=timeout) + except Exception as e: + self.logger.error(e) + return False + + def __process_destroyed() -> None: + session.detach() + + script: Script = session.create_script(self.script) + script.on('message', self.__process_message) + script.on('destroyed', __process_destroyed) + script.load() + + library = script.exports_sync.getlibrary(vendor.name) + if library: + self.logger.info('Library: %s (%s)', library['name'], library['path']) + + # Check if Ghidra XML functions loaded + if vendor.oem > 17 and not self.symbols: + self.logger.warning('For OEM API > 17, specifying "functions" is required, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md') + elif vendor.oem < 18 and self.symbols: + self.logger.warning('The "functions" attribute is deprecated for OEM API < 18') + + return script.exports_sync.hooklibrary(vendor.name) + + script.unload() + self.logger.warning('Library not found: %s' % vendor.name) + return False + + +__all__ = ('Core',) diff --git a/keydive/keydive.js b/keydive/keydive.js new file mode 100644 index 0000000..c10dadd --- /dev/null +++ b/keydive/keydive.js @@ -0,0 +1,299 @@ +/** + * Date: 2024-06-30 + * Description: DRM key extraction for research and educational purposes. + * Source: https://github.com/hyugogirubato/KeyDive + */ + +// Placeholder values dynamically replaced at runtime. +const OEM_CRYPTO_API = JSON.parse('${OEM_CRYPTO_API}'); +const NATIVE_C_API = JSON.parse('${NATIVE_C_API}'); +const SYMBOLS = JSON.parse('${SYMBOLS}'); + + +// Logging levels to synchronize with Python's logging module. +const Level = { + NOTSET: 0, + DEBUG: 10, + INFO: 20, + // WARN: WARNING, + WARNING: 30, + ERROR: 40, + // FATAL: CRITICAL, + CRITICAL: 50 +}; + +// Utility for encoding strings into byte arrays (UTF-8). +// https://gist.github.com/Yaffle/5458286#file-textencodertextdecoder-js +function TextEncoder() { +} + +TextEncoder.prototype.encode = function (string) { + const octets = []; + let i = 0; + while (i < string.length) { + const codePoint = string.codePointAt(i); + let c = 0; + let bits = 0; + if (codePoint <= 0x007F) { + c = 0; + bits = 0x00; + } else if (codePoint <= 0x07FF) { + c = 6; + bits = 0xC0; + } else if (codePoint <= 0xFFFF) { + c = 12; + bits = 0xE0; + } else if (codePoint <= 0x1FFFFF) { + c = 18; + bits = 0xF0; + } + octets.push(bits | (codePoint >> c)); + while (c >= 6) { + c -= 6; + octets.push(0x80 | ((codePoint >> c) & 0x3F)); + } + i += codePoint >= 0x10000 ? 2 : 1; + } + return octets; +}; + +// Simplified log function to handle messages and encode them for transport. +const print = (level, message) => { + message = message instanceof Object ? JSON.stringify(message) : message; + message = message ? new TextEncoder().encode(message) : message; + send(level, message); +} + + +// @Utils +const getLibraries = (name) => { + // https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792 + try { + const libraries = Process.enumerateModules(); + return libraries.filter(l => l.name.includes(name)); + } catch (e) { + print(Level.CRITICAL, e.message); + return []; + } +}; + +const getLibrary = (name) => { + const libraries = getLibraries(name); + return libraries.length === 1 ? libraries[0] : undefined; +} + +const getFunctions = (library) => { + try { + return library.enumerateExports(); + } catch (e) { + print(Level.CRITICAL, e.message); + return []; + } +} + + +// @Libraries +const UsePrivacyMode = (address) => { + // wvcdm::Properties::UsePrivacyMode + Interceptor.replace(address, new NativeCallback(function () { + return 0; + }, 'int', [])); + + Interceptor.attach(address, { + onEnter: function (args) { + print(Level.DEBUG, '[+] onEnter: UsePrivacyMode'); + }, + onLeave: function (retval) { + print(Level.DEBUG, '[-] onLeave: UsePrivacyMode'); + } + }); +} + +const GetCdmClientPropertySet = (address) => { + // wvcdm::Properties::GetCdmClientPropertySet + Interceptor.replace(address, new NativeCallback(function () { + return 0; + }, 'int', [])); + + Interceptor.attach(address, { + onEnter: function (args) { + print(Level.DEBUG, '[+] onEnter: GetCdmClientPropertySet'); + }, + onLeave: function (retval) { + print(Level.DEBUG, '[-] onLeave: GetCdmClientPropertySet'); + } + }); +} + +const PrepareKeyRequest = (address) => { + // wvcdm::CdmLicense::PrepareKeyRequest + Interceptor.attach(address, { + onEnter: function (args) { + print(Level.DEBUG, '[+] onEnter: PrepareKeyRequest'); + + // https://github.com/hyugogirubato/KeyDive/issues/13#issue-2327487249 + this.params = []; + for (let i = 0; i < 6; i++) { + this.params.push(args[i]); + } + }, + onLeave: function (retval) { + print(Level.DEBUG, '[-] onLeave: PrepareKeyRequest'); + let dumped = false; + + for (let i = 0; i < this.params.length; i++) { + try { + const param = ptr(this.params[i]); + const size = Memory.readUInt(param.add(Process.pointerSize)); + const data = Memory.readByteArray(param.add(Process.pointerSize * 2).readPointer(), size); + if (data) { + dumped = true; + send('challenge', data); + } + } catch (e) { + // print(Level.WARNING, `Failed to dump data for arg ${i}`); + } + } + !dumped && print(Level.ERROR, 'Failed to dump challenge.'); + } + }); +} + +const GetCertificatePrivateKey = (address, name) => { + // wvcdm::CryptoSession::GetCertificatePrivateKey + Interceptor.attach(address, { + onEnter: function (args) { + if (!args[6].isNull()) { + const size = args[6].toInt32(); + if (size >= 1000 && size <= 2000 && !args[5].isNull()) { + const buffer = args[5].readByteArray(size); + const bytes = new Uint8Array(buffer); + // Check for DER encoding markers for the beginning of a private key (MII). + if (bytes[0] === 0x30 && bytes[1] === 0x82) { + /* + let key = bytes; + try { + // Fixing key size + const binaryString = String.fromCharCode.apply(null, bytes); + const keyLength = getKeyLength(binaryString); // ASN.1 DER + key = bytes.slice(0, keyLength); + } catch (e) { + print(Level.ERROR, `${e.message} (${address})`); + } + */ + print(Level.DEBUG, `[*] GetCertificatePrivateKey: ${name}`); + !OEM_CRYPTO_API.includes(name) && print(Level.WARNING, `The function "${name}" does not belong to the referenced functions. Communicate it to the developer to improve the tool.`); + send('private_key', bytes); + } + } + } + }, + onLeave: function (retval) { + // print(Level.DEBUG, `[-] onLeave: ${name}`); + } + }); +} + +const getKeyLength = (key) => { + // Skip the initial tag + let pos = 1; + // Extract length byte, ignoring the long-form indicator bit + let lengthByte = key.charCodeAt(pos++) & 0x7F; + // If lengthByte indicates a short form, return early. + /* + if (lengthByte < 0x80) { + return pos + lengthByte; + } + */ + + // For long-form, calculate the length value. + let lengthValue = 0; + while (lengthByte--) { + lengthValue = (lengthValue << 8) + key.charCodeAt(pos++); + } + return pos + lengthValue; +} + +const GetDeviceId = (address, name) => { + // wvcdm::Properties::GetCdmClientPropertySet + Interceptor.attach(address, { + onEnter: function (args) { + print(Level.DEBUG, '[+] onEnter: getOemcryptoDeviceId'); + this.data = args[0]; + this.size = args[1]; + }, + onLeave: function (retval) { + print(Level.DEBUG, '[-] onLeave: getOemcryptoDeviceId'); + try { + const size = Memory.readPointer(this.size).toInt32(); + const data = Memory.readByteArray(this.data, size); + data && send('client_id', data); + } catch (e) { + print(Level.ERROR, `Failed to dump device Id.`); + } + } + }); +} + + +// @Hooks +const hookLibrary = (name) => { + // https://github.com/poxyran/misc/blob/master/frida-enumerate-imports.py + const library = getLibrary(name); + if (!library) return false; + + let functions; + if (SYMBOLS.length) { + // https://github.com/hyugogirubato/KeyDive/issues/13#issuecomment-2143741896 + functions = SYMBOLS.map(s => ({ + type: s.type, + name: s.name, + address: library.base.add(s.address) + })); + } else { + functions = getFunctions(library); + } + + functions = functions.filter(f => !NATIVE_C_API.includes(f.name)); + const targets = functions.filter(f => OEM_CRYPTO_API.includes(f.name)).map(f => f.name); + let hooked = 0; + + functions.forEach(func => { + if (func.type !== 'function') return; + const {name: funcName, address: funcAddr} = func; + + try { + if (funcName.includes('UsePrivacyMode')) { + UsePrivacyMode(funcAddr); + } else if (funcName.includes('GetCdmClientPropertySet')) { + GetCdmClientPropertySet(funcAddr); + } else if (funcName.includes('PrepareKeyRequest')) { + PrepareKeyRequest(funcAddr); + } else if (funcName.includes('getOemcryptoDeviceId')) { + GetDeviceId(funcAddr); + } else if (targets.includes(funcName) || (!targets.length && funcName.match(/^[a-z]+$/))) { + GetCertificatePrivateKey(funcAddr, funcName); + } else { + return; + } + + hooked++; + print(Level.DEBUG, `Hooked (${funcAddr}): ${funcName}`); + } catch (e) { + print(Level.ERROR, `${e.message} for ${funcName}`); + } + }); + + if (hooked < 3) { + print(Level.CRITICAL, 'Insufficient functions hooked.'); + return false; + } + + return true; +} + +// RPC interfaces exposed to external calls. +rpc.exports = { + getlibrary: getLibrary, + hooklibrary: hookLibrary +}; diff --git a/keydive/vendor.py b/keydive/vendor.py new file mode 100644 index 0000000..ca7854e --- /dev/null +++ b/keydive/vendor.py @@ -0,0 +1,32 @@ +class Vendor: + """ + Represents a Vendor with OEM, version, and name attributes. + """ + + def __init__(self, oem: int, version: str, name: str): + """ + Initializes a Vendor instance. + + Args: + oem (int): The OEM identifier. + version (str): The version of the vendor. + name (str): The name of the vendor. + """ + self.oem = oem + self.version = version + self.name = name + + def __repr__(self) -> str: + """ + Returns a string representation of the Vendor instance. + + Returns: + str: String representation of the Vendor instance. + """ + return '{name}({items})'.format( + name=self.__class__.__name__, + items=', '.join([f'{k}={repr(v)}' for k, v in self.__dict__.items()]) + ) + + +__all__ = ('Vendor',) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..93615c7 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,842 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "blinker" +version = "1.8.2" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.8" +files = [ + {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, + {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "construct" +version = "2.8.8" +description = "A powerful declarative parser/builder for binary data" +optional = false +python-versions = "*" +files = [ + {file = "construct-2.8.8.tar.gz", hash = "sha256:1b84b8147f6fd15bcf64b737c3e8ac5100811ad80c830cb4b2545140511c4157"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "flask" +version = "3.0.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.8" +files = [ + {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, + {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, +] + +[package.dependencies] +blinker = ">=1.6.2" +click = ">=8.1.3" +importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.0.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "frida" +version = "16.4.1" +description = "Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "frida-16.4.1-cp37-abi3-macosx_10_13_x86_64.whl", hash = "sha256:095d3d4b224f86d72472568ed6f2a44564bd7412783a785148cc766fc7cfcc68"}, + {file = "frida-16.4.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:11afd9a4b6c5bbe6a295c7d60271af578d0ec0024a196851498dfd3a75e8d55d"}, + {file = "frida-16.4.1-cp37-abi3-manylinux1_i686.whl", hash = "sha256:25f15e41bfc1b647ccab1b03ece46d2eccd8ad2150cfa77c1a20e1f7c6077f02"}, + {file = "frida-16.4.1-cp37-abi3-manylinux1_x86_64.whl", hash = "sha256:005b557081e87d7e7da32ba82e2db7d959c6d3b22600d0c510851dbd9481f2b2"}, + {file = "frida-16.4.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:0b69455642e6423f8794817045485e25e62c860867672b8c20b58314ccf619c8"}, + {file = "frida-16.4.1-cp37-abi3-manylinux2014_armv7l.whl", hash = "sha256:3e6ff9575208d0cc151c6bbed2f0265f3f0b4f10911b0434069eb0354622e0d4"}, + {file = "frida-16.4.1-cp37-abi3-manylinux_2_17_aarch64.whl", hash = "sha256:4bfd7dc350aae3d46f9e06a5cb49651ce796ac9b3db08338917cccdc36660ba0"}, + {file = "frida-16.4.1-cp37-abi3-manylinux_2_17_armv7l.whl", hash = "sha256:774583b492fe73e0199bc9d49fadb5c2d0332f0c77b076efa888d4995ad26a60"}, + {file = "frida-16.4.1-cp37-abi3-manylinux_2_5_i686.whl", hash = "sha256:473cd7fea8e80973553a79ca6c11d070ffe79f3bc7a2f03696cc80d22b6525e1"}, + {file = "frida-16.4.1-cp37-abi3-manylinux_2_5_x86_64.whl", hash = "sha256:e0f073aef8974530b693f750615b289088538796b27198faf81780e1fdc6c040"}, + {file = "frida-16.4.1-cp37-abi3-win32.whl", hash = "sha256:4569057c02064fb7cd6d790469631e3c56f49e0a62bb4045d6d13cb76a05ede4"}, + {file = "frida-16.4.1-cp37-abi3-win_amd64.whl", hash = "sha256:ff8f82f4518ef4335d2d2dcf6de9c57a656aae50aa5ca272bb0d53b403a00a62"}, + {file = "frida-16.4.1.tar.gz", hash = "sha256:c76e22323a80d4f9d22c0813c690ef956e6d55add223cbc533892e045a19e955"}, +] + +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.11\""} + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "importlib-metadata" +version = "8.0.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, + {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "pathlib" +version = "1.0.1" +description = "Object-oriented filesystem paths" +optional = false +python-versions = "*" +files = [ + {file = "pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147"}, + {file = "pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "protobuf" +version = "4.25.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "pycryptodome" +version = "3.20.0" +description = "Cryptographic library for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, + {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, + {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, + {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, + {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, + {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, + {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, + {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, + {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, + {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, + {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, + {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, + {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "pycryptodomex" +version = "3.20.0" +description = "Cryptographic library for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, + {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, + {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, + {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "pymp4" +version = "1.4.0" +description = "Python parser for MP4 boxes" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pymp4-1.4.0-py3-none-any.whl", hash = "sha256:3401666c1e2a97ac94dffb18c5a5dcbd46d0a436da5272d378a6f9f6506dd12d"}, + {file = "pymp4-1.4.0.tar.gz", hash = "sha256:bc9e77732a8a143d34c38aa862a54180716246938e4bf3e07585d19252b77bb5"}, +] + +[package.dependencies] +construct = "2.8.8" + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "pyreadline3" +version = "3.4.1" +description = "A python implementation of GNU readline." +optional = false +python-versions = "*" +files = [ + {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, + {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "pywidevine" +version = "1.8.0" +description = "Widevine CDM (Content Decryption Module) implementation in Python." +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "pywidevine-1.8.0-py3-none-any.whl", hash = "sha256:1ecf029ce562789b18bbbd64604596d15645aadf413b255cf0fafc8d8b06659d"}, + {file = "pywidevine-1.8.0.tar.gz", hash = "sha256:c14f3fe2864473416b9caa73d9a21251a02d72138e6d54d8c1a3f44b7a6b05c9"}, +] + +[package.dependencies] +click = ">=8.1.7,<9.0.0" +protobuf = ">=4.25.1,<5.0.0" +pycryptodome = ">=3.19.0,<4.0.0" +pymp4 = ">=1.4.0,<2.0.0" +PyYAML = ">=6.0.1,<7.0.0" +requests = ">=2.31.0,<3.0.0" +Unidecode = ">=1.3.7,<2.0.0" + +[package.extras] +serve = ["aiohttp (>=3.9.1,<4.0.0)"] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "unidecode" +version = "1.3.8" +description = "ASCII transliterations of Unicode text" +optional = false +python-versions = ">=3.5" +files = [ + {file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"}, + {file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "werkzeug" +version = "3.0.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, + {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +optional = false +python-versions = ">=3.4" +files = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[[package]] +name = "zipp" +version = "3.19.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, + {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, +] + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[package.source] +type = "legacy" +url = "https://pypi.org/simple" +reference = "localpypi" + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "0976c2b2ac220d7a9dc2b0d17acd15c0d6e012e8d5c2eded1c63ae0346e59a74" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..92ede19 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "keydive" +version = "2.0.0" +description = "Extract Widevine L3 keys from Android devices effortlessly, spanning multiple Android versions for DRM research and education." +license = "MIT" +authors = ["hyugogirubato <65763543+hyugogirubato@users.noreply.github.com>"] +readme = "README.md" +repository = "https://github.com/hyugogirubato/KeyDive" +keywords = ["python", "drm", "widevine", "google"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Natural Language :: English", + "Operating System :: OS Independent", + "Topic :: Multimedia :: Video", + "Topic :: Security :: Cryptography", + "Topic :: Software Development :: Libraries :: Python Modules" +] +include = [ + { path = "CHANGELOG.md", format = "sdist" }, + { path = "README.md", format = "sdist" }, + { path = "LICENSE", format = "sdist" } +] + +[tool.poetry.urls] +"Issues" = "https://github.com/hyugogirubato/KeyDive/issues" +"Packages" = "https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md" +"Functions" = "https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md" +"Changelog" = "https://github.com/hyugogirubato/KeyDive/blob/main/CHANGELOG.md" + +[tool.poetry.dependencies] +python = "^3.8" +coloredlogs = "^15.0.1" +frida = "^16.2.2" +pathlib = "^1.0.1" +pycryptodomex = "^3.20.0" +xmltodict = "^0.13.0" +pywidevine = "^1.8.0" +PyYAML = "^6.0.1" +Flask = "^3.0.3" + +[tool.poetry.scripts] +keydive = "keydive.__main__:main" + +[[tool.poetry.source]] +name = "localpypi" +url = "https://pypi.org/simple/" +priority = "primary" + +[certificates] +localpypi = { cert = false } + + diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index bdd3adf..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -frida~=16.2.5 -pathlib~=1.0.1 -coloredlogs~=15.0.1 -pycryptodomex~=3.20.0 -xmltodict~=0.13.0 -PyYAML~=6.0.1 -Flask~=3.0.3 -pywidevine~=1.8.0 \ No newline at end of file