From 82d6a903dd145956cd3e1543c5a4d5dbc43c5db8 Mon Sep 17 00:00:00 2001 From: hyugogirubato <65763543+hyugogirubato@users.noreply.github.com> Date: Mon, 9 Jun 2025 18:12:49 +0200 Subject: [PATCH] release v3.0.0 --- .gitignore | 16 +- CHANGELOG.md | 62 ++ DEVELOPERS.md | 92 ++ LICENSE => LICENSE.txt | 2 +- README.md | 118 +-- docs/advanced/CHALLENGE.md | 2 +- docs/advanced/FUNCTIONS.md | 6 +- docs/server/server.py | 109 +- keydive/__init__.py | 10 +- keydive/__main__.py | 233 ++--- keydive/adb.py | 287 ------ keydive/adb/__init__.py | 171 ++++ keydive/adb/remote.py | 371 +++++++ keydive/adb/vendor.py | 114 +++ keydive/cdm.py | 279 ------ keydive/constants.py | 145 --- keydive/core.py | 552 +++++++--- keydive/drm/__init__.py | 55 + keydive/drm/cdm.py | 1000 ++++++++++++++++++ keydive/drm/device.py | 113 +++ keydive/drm/keybox.py | 243 +++++ keydive/drm/protocol/license_pb2.py | 352 +++++++ keydive/drm/protocol/license_pb2.pyi | 1391 ++++++++++++++++++++++++++ keydive/keybox.py | 180 ---- keydive/keydive.js | 817 ++++++++++----- keydive/utils.py | 213 ++++ keydive/vendor.py | 34 - pyproject.toml | 37 +- 28 files changed, 5384 insertions(+), 1620 deletions(-) create mode 100644 DEVELOPERS.md rename LICENSE => LICENSE.txt (96%) delete mode 100644 keydive/adb.py create mode 100644 keydive/adb/__init__.py create mode 100644 keydive/adb/remote.py create mode 100644 keydive/adb/vendor.py delete mode 100644 keydive/cdm.py delete mode 100644 keydive/constants.py create mode 100644 keydive/drm/__init__.py create mode 100644 keydive/drm/cdm.py create mode 100644 keydive/drm/device.py create mode 100644 keydive/drm/keybox.py create mode 100644 keydive/drm/protocol/license_pb2.py create mode 100644 keydive/drm/protocol/license_pb2.pyi delete mode 100644 keydive/keybox.py create mode 100644 keydive/utils.py delete mode 100644 keydive/vendor.py diff --git a/.gitignore b/.gitignore index 8c249d4..952423e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ @@ -170,10 +173,11 @@ poetry.toml # LSP config files pyrightconfig.json -### KeyDive ### -*.xml +# End of https://www.toptal.com/developers/gitignore/api/python + +# KeyDive device/ -logs/ -/docs/server/curl.txt -/docs/server/provisioning.json -/docs/debug.js +docs/pdf/ +docs/protos/ +docs/scripts/ +*.proto \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bffdadb..e450948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,67 @@ 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). +## [3.0.0] - 2025-06-09 + +### Added + +- Support for OTA provisioning. +- Dumping of OEM Device Certificate to allow manual L3 provisioning without a keybox. +- Dynamic keybox generation. +- Detection of keybox token during provisioning (including L1 support when `device_aes_key` is provided). +- New challenge interception function (TODO: may reduce dump failures?). +- Option `--no-stop` to keep capture running after requirement is met. +- Debug display of DRM player PID. +- Debug detection and display of default browser PID (supports Google Chrome, Samsung Internet, Mozilla Firefox). +- Display of client capabilities in debug mode. +- Full JSON-formatted output for client information. +- New private function hooks. +- Support and backward compatibility for Frida API 17+. +- Ctrl+C support when analyzing the device (ADB shell command). +- New contributor: [samu87d8dh2](https://github.com/samu87d8dh2) + +### Changed + +- All C API functions are now filtered. +- Standardized JS hook functions. +- The keybox is now handled as an object rather than a separate process. +- DRM information parsing (keybox, device ID, challenge, token, etc.) is now centralized in a single class. +- Constants are now split per module instead of being centralized in a single file. +- Widevine license protobuf updated to 2020 version (partially compatible with CDM 19+). +- `cryptography` is now used instead of `pycryptodomex`. +- CDM is now resolved with improved accuracy (security level, system ID). +- Keybox level is now validated against the SDK. +- Standardized Frida JS script file reading functions. +- Clearer output for `-a player` or `-a web` options. +- Deprecated script message is now shown only once, at the first hook. +- Data export now occurs after every relevant event (optimization). +- CDM search is performed in descending version order. +- Index for extracting `client_id` argument has been adjusted. +- File names in generated tree are now normalized using `unidecode`. + +### Fixed + +- Process name resolution for Widevine DRM process. +- Missing hook on file read function. +- Vendor model updated to support library checking via regex and fix rendering. +- Updated function allowlist for Ghidra-based function analysis. +- ADB process listing fix (handles multiple entries with same name but different PIDs). +- `dumpsys` check for application package verification. +- DRM player app is no longer relaunched if already running (even in background). +- Frida server version is retrieved and displayed only once. +- Keybox is fully parsed only when decrypted data is available. +- CRC32 check added for keybox validation. +- Improved display when encrypted keybox is received (no more invalid output). +- Regex fix in process analysis. +- Proper handling of `getprop` output to conform with expected format. +- Removed dependency on `pywidevine` and unnecessary associated libraries. +- Option to force plaintext challenge added (disabled by default; encrypted interception now works). +- Better resolution of CDM level and security parameters. + +### New Contributors + +- [samu87d8dh2](https://github.com/samu87d8dh2) + ## [2.2.1] - 2025-03-01 ### Added @@ -446,6 +507,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. +[3.0.0]: https://github.com/hyugogirubato/KeyDive/releases/tag/v3.0.0 [2.2.1]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.2.1 [2.2.0]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.2.0 [2.1.5]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.1.5 diff --git a/DEVELOPERS.md b/DEVELOPERS.md new file mode 100644 index 0000000..2d1c28b --- /dev/null +++ b/DEVELOPERS.md @@ -0,0 +1,92 @@ +# KeyDive Developer Documentation + +This document explains the internal workings of KeyDive, including key concepts in the JS hook scripts, the meaning of critical fields, and the flow of the extraction process. + +## Key Concepts & Terminology + +| Key | Description | +|-----------------------|-------------------------------------------------------------------------------------------| +| `provisioning_method` | Indicates the method used to provision Widevine DRM. | +| `challenge` | Unencrypted challenge data sent to Widevine CDM to trigger provisioning or key requests. | +| `client_id` | Unique identifier extracted from the device representing the Widevine client credentials. | +| `private_key` | RSA private key used for DRM content decryption and authentication. | +| `keybox` | Device-specific key storage blob containing keys for DRM operations. | +| `certificate` | Device OEM certificate chain for verifying keys and trust. | + +## Script Execution Flow + +The core extraction logic is implemented in a Frida hook script injected into Widevine CDM processes. The following sequence diagram illustrates the general flow, including options for player choice and output formats: + +```` ++----------------+ +------------------+ +---------------------+ +------------------+ +| Start Script | | Select Player | | Provisioning Method | | Extraction Steps | ++----------------+ +------------------+ +---------------------+ +------------------+ +| | | | +|--(option: -a player)-->| Launch Kaltura app | | +| | | | +|--(option: -a web)----->| Launch Bitmovin player | | +| | | | +| | |---Check provisioning_method-->| +| | | | +| | |<--Send challenge data---------| +| | | | +| | |---Extract client_id---------->| +| | |---Extract private_key-------->| +| | |---Extract keybox (optional)-->| +| | | | +| | |---Export data to output dir-->| +| | | | +| | | | +V V V V +(Process ends) (Process ends) (Process ends) (Process ends) +```` + +## Key Data Handling + +### Provisioning Method + +The provisioning method determines how the device is authenticated with Widevine. Internally, KeyDive uses the following enum: + +| Enum Value | Meaning | +|---------------------|-----------------------------------------------------------------------------| +| `ProvisioningError` | Device cannot be provisioned (typically unsupported or failed setup). | +| `DrmCertificate` | The device uses a pre-installed baked-in DRM certificate (usually L3 only). | +| `Keybox` | A factory-installed keybox is used to identify the device and decrypt keys. | +| `OEMCertificate` | A factory-provisioned OEM certificate chain is present for full L1 support. | + +These values are automatically detected at runtime to determine the correct extraction strategy. + +### Challenge Handling + +The `challenge` is a critical blob of data sent during provisioning or license acquisition. KeyDive can: + +- Intercept it during DRM playback. +- Accept an external file via the `--challenge` argument. + +This challenge contains the encrypted request for keys or certificates and is essential for key extraction workflows. + +### Client ID & Private Key + +Both the `client_id` and `private_key` are extracted from the intercepted CDM functions. They are: + +- Required for device emulation or license response reconstruction. +- Automatically exported to formats like `client_id.bin`, `private_key.pem`, and `.wvd`. + +## Advanced Options & Debugging + +The advanced command-line options allow: + +- Overriding automatic provisioning detection. +- Injecting Ghidra-extracted symbol maps via `--symbols`. +- Forcing use of unencrypted challenges (`--unencrypt`) for debugging or fallback workflows. +- Extracting optional blobs like the Widevine keybox or certificates. + +These are primarily useful for development, debugging, or supporting less common device models. + +## Contribution Guidelines + +Feel free to contribute by submitting PRs or reporting issues. Please follow the coding style and provide tests for new features. + +--- + +© hyugogirubato 2025 \ No newline at end of file diff --git a/LICENSE b/LICENSE.txt similarity index 96% rename from LICENSE rename to LICENSE.txt index 76601fc..a15bc43 100644 --- a/LICENSE +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 hyugogirubato +Copyright (c) 2025 hyugogirubato Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ec0d999..943759a 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,26 @@ -# KeyDive: Widevine L3 Extractor for Android +# KeyDive: Widevine L3 Key Extractor for Android -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. +KeyDive is a Python tool designed to extract Widevine L3 DRM keys from Android devices seamlessly, supporting multiple Android versions for DRM research, education, and analysis. > [!IMPORTANT] -> A minimum version of `frida-server 16.6.0` is required for dynamic dumps on OEM API 18+ (SDK > 33). Otherwise, extracted functions from Ghidra are required. +> For dynamic key extraction on devices with Android SDK > 33 (OEM API 18+), a minimum `frida-server 16.6.0` is required. Otherwise, pre-extracted functions from Ghidra are necessary. ## Features -- 🚀 Seamless Installation via [pip](#installation) -- 🔄 Automated extraction of Widevine L3 DRM keys -- 📱 Compatibility with a wide range of Android versions (SDK > 21), ensuring broad applicability -- 💾 Seamless extraction process, yielding essential DRM components -- 🌐 Offline extraction mode for environments without internet access -- 🖥️ Command-line options for flexible usage -- 🛠️ Support for custom functions extracted from Widevine libraries using Ghidra -- ❤️ Fully Open-Source! Pull Requests Welcome +- 🚀 Easy installation with [pip](https://pip.pypa.io/) +- 🔄 Automated Widevine L3 key extraction supporting SDK > 21 +- 📱 Supports a wide range of Android devices and versions +- 💾 Export extracted keys and device credentials in multiple formats including pywidevine `.wvd` +- 🌐 Offline extraction mode available +- 🖥️ Flexible command-line interface with multiple customization options +- 🛠️ Supports injecting custom Widevine functions extracted from Ghidra XML files +- ❤️ Fully open-source and actively maintained ## Prerequisites -Before you begin, ensure you have the following prerequisites in place: - -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/). +- **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. +- **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/). +- **Python 3.8+** ## Installation @@ -29,18 +28,16 @@ Follow these steps to set up KeyDive: 1. Ensure all prerequisites are met (see above). 2. Install KeyDive directly from PyPI: - ```shell + ````shell pip install keydive - ``` + ```` ## Usage -KeyDive enables secure extraction of Widevine L3 keys in a straightforward sequence: - 1. Run the KeyDive script: - ```bash - keydive -kwp - ``` + ````bash + keydive -kw -a player + ```` 2. The script will install and launch the [Kaltura](https://github.com/kaltura/kaltura-device-info-android) DRM test app (if not already installed). 3. Follow these steps within the app: - **Provision Widevine** (if the device isn't provisioned). @@ -50,67 +47,73 @@ KeyDive enables secure extraction of Widevine L3 keys in a straightforward seque - `client_id.bin` (device identification data). - `private_key.pem` (RSA private key). -This sequence ensures that the DRM-protected content is active and ready for key extraction by the time the KeyDive script is initiated, optimizing the extraction process. +This will automatically install and launch the recommended DRM test app (Kaltura), provision Widevine if necessary, and perform the extraction steps. + +> [!TIP] +> This sequence ensures that the DRM-protected content is active and ready for key extraction by the time the KeyDive script is initiated, optimizing the extraction process. ### Command-Line Options -```shell -usage: keydive [-h] [-d ] [-v] [-l ] [--delay ] [--version] [-o ] [-w] [-s] [-a] [-p] [-f ] [-k] [--challenge ] [--private-key ] +````shell +Usage: keydive [-h] [-s ] [-d ] [-v] [-l ] [-V] [-o ] [-w] [-k] [-a ] [--no-detect] [--no-disabler] [--no-stop] [--unencrypt] [--symbols ] [--challenge ] [--rsa-key ] [--aes-key ] -Extract Widevine L3 keys from an Android device. +Extract Widevine CDM components from an Android device. -options: +Optional Arguments: -h, --help show this help message and exit -Global: - -d , --device - Specify the target Android device ID for ADB connection. - -v, --verbose Enable verbose logging for detailed debug output. - -l , --log - Directory to store log files. - --delay Delay (in seconds) between process checks. - --version Display KeyDive version information. +Global Options: + -s, --serial + ADB serial number of the target Android device. + -d, --delay Delay in seconds between process status checks. (default: 1.0) + -v, --verbose Enable detailed logging for debugging. + -l, --log Directory to save log files. + -V, --version Show tool version and exit. -Cdm: - -o , --output - Output directory for extracted data. - -w, --wvd Generate a pywidevine WVD device file. - -s, --skip Skip auto-detection of the private function. - -a, --auto Automatically start the Bitmovin web player. - -p, --player Install and start the Kaltura app automatically. +Cdm Extraction: + -o, --output Directory to store extracted CDM files. (default: ./device) + -w, --wvd Export data in pywidevine-compatible WVD format. + -k, --keybox Export Widevine keybox if available on the device. + -a, --auto Automatically launch a DRM playback test. ("web" or "player") -Advanced: - -f , --functions - Path to Ghidra XML functions file. - -k, --keybox Enable export of the Keybox data if it is available. - --challenge Path to unencrypted challenge for extracting client ID. - --private-key Path to private key for extracting client ID. +Advanced Options: + --no-detect Disable automatic detection of OEM private key function. + --no-disabler Disable liboemcrypto-disabler module (patches memory protection). + --no-stop Do not stop once minimum CDM data is intercepted. + --unencrypt Force the license challenge to keep client ID data unencrypted. + --symbols Path to Ghidra-generated XML symbol file for function mapping. + --challenge + Protobuf challenge file(s) captured via MITM proxy. + --rsa-key RSA private key(s) in PEM or DER format for client ID decryption. + --aes-key AES key(s) in hex, base64, or file form for decrypting keybox data. +```` -``` +> [!NOTE] +> The advanced options are primarily intended for debugging and development purposes. Regular users do not need to use them. ## Advanced Usage ### Extracting Functions -For advanced users looking to use custom functions with KeyDive, a comprehensive guide on extracting functions from Widevine libraries using Ghidra is available. Please refer to our [Functions Extraction Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/advanced/FUNCTIONS.md) for detailed instructions. +Custom functions extracted from Widevine libraries with Ghidra can be provided to KeyDive to improve compatibility on some devices. See the [Functions Extraction Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/advanced/FUNCTIONS.md). ### Offline Extraction -KeyDive supports offline extraction mode for situations without internet access. This mode allows you to extract DRM keys directly from your Android device. Ensure all necessary dependencies are installed and follow the detailed [Offline Mode Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/advanced/OFFLINE.md) for step-by-step instructions. +KeyDive supports offline extraction workflows suitable for restricted environments. See the [Offline Mode Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/advanced/OFFLINE.md). -### Obtaining Unencrypted Challenge Data +### Using Unencrypted Challenge Data -> [!NOTE] -> Usage of unencrypted challenge is not required by default. It is only necessary when the script cannot extract the client id. +> [!CAUTION] +> The `--unencrypt` option forces the license challenge to keep client ID data unencrypted. This option can cause repetitive crashes or instability in the Widevine library on certain devices. -To extract the unencrypted challenge data required for KeyDive's advanced features, follow the steps outlined in our [Challenge Extraction Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/advanced/CHALLENGE.md). This data is crucial for analyzing DRM-protected content and enhancing your DRM key extraction capabilities. +When client ID extraction fails, provide an unencrypted challenge via `--challenge`. See the [Challenge Extraction Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/advanced/CHALLENGE.md). ### Temporary Disabling L1 for L3 Extraction > [!WARNING] > Usage of the module is now deprecated because the deactivation of the library was natively added. -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. +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. ## Disclaimer @@ -124,6 +127,7 @@ KeyDive is intended for educational and research purposes only. The use of this JohnDoe1964 Nineteen93 sn-o-w +samu87d8dh2 ## Licensing @@ -132,4 +136,4 @@ You can find a copy of the license in the LICENSE file in the root folder. --- -© hyugogirubato 2025 \ No newline at end of file +© hyugogirubato 2025 diff --git a/docs/advanced/CHALLENGE.md b/docs/advanced/CHALLENGE.md index 7ccdd60..6599ef0 100644 --- a/docs/advanced/CHALLENGE.md +++ b/docs/advanced/CHALLENGE.md @@ -56,7 +56,7 @@ To extract unencrypted challenges using KeyDive and HTTP Toolkit, you can follow 6. **Use Extracted Data:** - Use the extracted data with KeyDive by running: ```shell - keydive --device --challenge path/to/challenge + keydive --serial --challenge path/to/challenge ``` Replace `path/to/challenge` with the actual path to the extracted challenge data file. diff --git a/docs/advanced/FUNCTIONS.md b/docs/advanced/FUNCTIONS.md index 2604f93..dbcbde1 100644 --- a/docs/advanced/FUNCTIONS.md +++ b/docs/advanced/FUNCTIONS.md @@ -9,7 +9,7 @@ To utilize custom functions with KeyDive, particularly when extracting Widevine Run KeyDive to detect the library path on the Android device: ```shell -keydive --device +keydive --serial ``` Replace `` with the ID of your Android device connected via ADB. @@ -77,9 +77,9 @@ Ensure you have Ghidra installed on your system. If not, download it from the [G Once you have the `functions.xml` file: - Ensure KeyDive is set up according to its documentation. -- When running KeyDive, use the `--functions` argument to specify the path to your `functions.xml` file. For example: +- When running KeyDive, use the `--symbols` argument to specify the path to your `functions.xml` file. For example: ```shell - keydive --device --functions /path/to/functions_x86.xml + keydive --serial --symbols /path/to/functions_x86.xml ``` - Proceed with the key extraction process as detailed in KeyDive's usage instructions. diff --git a/docs/server/server.py b/docs/server/server.py index b681f6d..c39c3f7 100644 --- a/docs/server/server.py +++ b/docs/server/server.py @@ -1,25 +1,26 @@ -import argparse import json import time import logging +from argparse import ArgumentParser from pathlib import Path import requests from flask import Flask, Response, request, redirect +from rich_argparse import RichHelpFormatter -from keydive.__main__ import configure_logging +from keydive.utils import configure_logging # Suppress urllib3 warnings -logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR) +logging.getLogger('urllib3.connectionpool').setLevel(logging.ERROR) # Initialize Flask application app = Flask(__name__) # Define paths, constants, and global flags PARENT = Path(__file__).parent -VERSION = "1.0.1" +VERSION = '1.0.2' KEYBOX = False DELAY = 10 @@ -30,9 +31,9 @@ def health_check() -> Response: Health check endpoint to confirm the server is running. Returns: - Response: A simple "pong" message with a 200 OK status. + Response: A simple 'pong' message with a 200 OK status. """ - return Response(response="pong", status=200, content_type="text/html; charset=utf-8") + return Response(response='pong', status=200, content_type='text/html; charset=utf-8') @app.route('/shaka-demo-assets/angel-one-widevine/', methods=['GET']) @@ -41,17 +42,17 @@ def shaka_demo_assets(file) -> Response: Serves cached assets for Widevine demo content. If the requested file is not available locally, it fetches it from a remote server and caches it. - Parameters: + Args: file (str): File path requested by the client. Returns: Response: File content as a byte stream, or a 404 error if not found. """ - logger = logging.getLogger("Shaka") - logger.info("%s %s", request.method, request.path) + logger = logging.getLogger('Shaka') + logger.info('%s %s', request.method, request.path) try: - path = PARENT / ".assets" / file + path = PARENT / '.assets' / file path.parent.mkdir(parents=True, exist_ok=True) if path.is_file(): @@ -60,20 +61,20 @@ def shaka_demo_assets(file) -> Response: else: # Fetch the file from remote storage if not cached locally r = requests.get( - url=f"https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/{file}", + url=f'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/{file}', headers={ - "Accept": "*/*", - "User-Agent": "KalturaDeviceInfo/1.4.1 (Linux;Android 10) ExoPlayerLib/2.9.3" + 'Accept': '*/*', + 'User-Agent': 'KalturaDeviceInfo/1.4.1 (Linux;Android 10) ExoPlayerLib/2.9.3' } ) r.raise_for_status() path.write_bytes(r.content) # Cache the downloaded content content = r.content - logger.debug("Downloaded assets: %s", path) + logger.debug('Downloaded assets: %s', path) - return Response(response=content, status=200, content_type="application/octet-stream") + return Response(response=content, status=200, content_type='application/octet-stream') except Exception as e: - return Response(response=str(e), status=404, content_type="text/html; charset=utf-8") + return Response(response=str(e), status=404, content_type='text/html; charset=utf-8') @app.route('/certificateprovisioning/v1/devicecertificates/create', methods=['POST']) @@ -87,16 +88,16 @@ def certificate_provisioning() -> Response: Response: JSON response if provisioning is complete, else a redirection. """ global KEYBOX, DELAY - logger = logging.getLogger("Google") - logger.info("%s %s", request.method, request.path) + logger = logging.getLogger('Google') + logger.info('%s %s', request.method, request.path) if KEYBOX: - logger.warning("Provisioning request aborted to prevent keybox spam") - return Response(response="Internal Server Error", status=500, content_type="text/html; charset=utf-8") + logger.warning('Provisioning request aborted to prevent keybox spam') + return Response(response='Internal Server Error', status=500, content_type='text/html; charset=utf-8') # Generate a curl command from the incoming request for debugging or testing - user_agent = request.headers.get("User-Agent", "Unknown") - url = request.url.replace("http://", "https://") + user_agent = request.headers.get('User-Agent', 'Unknown') + url = request.url.replace('http://', 'https://') prompt = [ 'curl', '--request', 'POST', @@ -109,15 +110,15 @@ def certificate_provisioning() -> Response: ] # Save the curl command for potential replay or inspection - curl = PARENT / "curl.txt" - curl.write_text(" \\\n ".join(prompt)) - logger.debug("Saved curl command to: %s", curl) + curl = PARENT / 'curl.txt' + curl.write_text(' \\\n '.join(prompt)) + logger.debug('Saved curl command to: %s', curl) # Wait for provisioning response data with retries - logger.warning("Waiting for provisioning response...") - provision = PARENT / "provisioning.json" + logger.warning('Waiting for provisioning response...') + provision = PARENT / 'provisioning.json' provision.unlink(missing_ok=True) - provision.write_bytes(b"") # Create empty file for manual input if needed + provision.write_bytes(b'') # Create empty file for manual input if needed # Poll for the presence of a response up to DELAY times with 1-second intervals for _ in range(DELAY): @@ -127,13 +128,13 @@ def certificate_provisioning() -> Response: # Cleanup after successful response curl.unlink(missing_ok=True) provision.unlink(missing_ok=True) - return Response(response=content, status=200, content_type="application/json") + return Response(response=content, status=200, content_type='application/json') except Exception as e: pass # Continue waiting if file is empty or not yet ready time.sleep(1) # Redirect to the secure URL if response is not available - logger.warning("Redirecting to avoid timeout") + logger.warning('Redirecting to avoid timeout') return redirect(url, code=302) @@ -143,32 +144,32 @@ def main() -> None: to set global parameters and configures logging, then starts the Flask server. """ global VERSION, DELAY, KEYBOX - parser = argparse.ArgumentParser(description="Local DRM provisioning video player.") + parser = ArgumentParser( + description='Local DRM provisioning video player.', + formatter_class=RichHelpFormatter) - # Global arguments for the application - group_global = parser.add_argument_group("Global") - group_global.add_argument('--host', required=False, type=str, default="127.0.0.1", metavar="", help="Host address for the server to bind to.") - group_global.add_argument('--port', required=False, type=int, default=9090, metavar="", help="Port number for the server to listen on.") - group_global.add_argument('-v', '--verbose', required=False, action="store_true", help="Enable verbose logging for detailed debug output.") - group_global.add_argument('-l', '--log', required=False, type=Path, metavar="", help="Directory to store log files.") - group_global.add_argument('--version', required=False, action="store_true", help="Display Server version information.") + global_group = parser.add_argument_group('Global Options') + global_group.add_argument('--host', type=str, default='127.0.0.1', metavar='', help='Host address for the server to bind to.') + global_group.add_argument('--port', type=int, default=9090, metavar='', help='Port number for the server to listen on.') + global_group.add_argument('-v', '--verbose', action='store_true', help='Enable detailed logging for debugging.') + global_group.add_argument('-l', '--log', type=Path, metavar='', help='Directory to save log files.') + global_group.add_argument('--version', action='store_true', help='Show tool version and exit.') - # Advanced options - group_advanced = parser.add_argument_group("Advanced") - group_advanced.add_argument('-d', '--delay', required=False, type=int, metavar="", default=10, help="Delay (in seconds) between successive checks for provisioning responses.") - group_advanced.add_argument('-k', '--keybox', required=False, action="store_true", help="Enable keybox mode, which aborts provisioning requests to prevent spam.") + advanced_group = parser.add_argument_group('Advanced Options') + advanced_group.add_argument('-d', '--delay', type=int, metavar='', default=10, help='Delay (in seconds) between successive checks for provisioning responses.') + advanced_group.add_argument('-k', '--keybox', action='store_true', help='Enable keybox mode, which aborts provisioning requests to prevent spam.') args = parser.parse_args() - # Handle version display + # Handle version flag early and exit if args.version: - print(f"Server {VERSION}") - exit(0) + print(f'Server {VERSION}') + return - # Configure logging (file and console) + # Set up logging (to file if specified, otherwise stdout) log_path = configure_logging(path=args.log, verbose=args.verbose) - logger = logging.getLogger("Server") - logger.info("Version: %s", VERSION) + logger = logging.getLogger('Server') + logger.info('Version: %s', VERSION) try: # Set global variables based on parsed arguments @@ -176,18 +177,20 @@ def main() -> None: KEYBOX = args.keybox # Start Flask app with specified host, port, and debug mode - logging.getLogger("werkzeug").setLevel(logging.INFO if args.verbose else logging.ERROR) + logging.getLogger('werkzeug').setLevel(logging.INFO if args.verbose else logging.ERROR) app.run(host=args.host, port=args.port, debug=False) except KeyboardInterrupt: + # Graceful exit on user interrupt (Ctrl+C) pass except Exception as e: logger.critical(e, exc_info=args.verbose) - # Final logging and exit + # Log the path to the log file if it was created if log_path: - logger.info("Log file: %s" % log_path) - logger.info("Exiting") + logger.info('Log file: %s' % log_path) + + logger.info('Exiting') -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/keydive/__init__.py b/keydive/__init__.py index 3251fa3..47af1b1 100644 --- a/keydive/__init__.py +++ b/keydive/__init__.py @@ -1,7 +1,7 @@ from .core import Core -from .adb import ADB -from .cdm import Cdm -from .vendor import Vendor -from .keybox import Keybox +from .adb.remote import Remote +from .adb.vendor import Vendor +from .drm.cdm import Cdm +from .drm.keybox import KeyBox -__version__ = "2.2.1" +__version__ = '3.0.0' diff --git a/keydive/__main__.py b/keydive/__main__.py index 93bfd24..3a1f07b 100644 --- a/keydive/__main__.py +++ b/keydive/__main__.py @@ -1,208 +1,95 @@ -import argparse import logging -import time -from datetime import datetime from pathlib import Path -from typing import Optional +from argparse import ArgumentParser -import coloredlogs +from rich_argparse import RichHelpFormatter import keydive - -from keydive.adb import ADB -from keydive.cdm import Cdm -from keydive.constants import CDM_VENDOR_API, DRM_PLAYER from keydive.core import Core - - -def configure_logging(path: Path = None, verbose: bool = False) -> Optional[Path]: - """ - Configures logging for the application. - - Parameters: - path (Path, optional): The directory to store log files. - verbose (bool, optional): Flag to enable detailed debug logging. - - Returns: - Path: The path of the log file, or None if no log file is created. - """ - # Set up the root logger with the desired logging level - 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: - # Ensure the log directory exists - 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) - - # Set log formatting - 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 +from keydive.utils import configure_logging def main() -> None: - """ - Main entry point for the KeyDive application. + parser = ArgumentParser( + description='Extract Widevine CDM components from an Android device.', + formatter_class=RichHelpFormatter) - This application extracts Widevine L3 keys from an Android device. - It supports device management via ADB and allows hooking into Widevine processes. - """ - parser = argparse.ArgumentParser(description="Extract Widevine L3 keys from an Android device.") + global_group = parser.add_argument_group('Global Options') + global_group.add_argument('-s', '--serial', type=str, metavar='', help='ADB serial number of the target Android device.') + global_group.add_argument('-d', '--delay', type=float, metavar='', default=1.0, help='Delay in seconds between process status checks. (default: 1.0)') + global_group.add_argument('-v', '--verbose', action='store_true', help='Enable detailed logging for debugging.') + global_group.add_argument('-l', '--log', type=Path, metavar='', help='Directory to save log files.') + global_group.add_argument('-V', '--version', action='store_true', help='Show tool version and exit.') - # Global arguments for the application - group_global = parser.add_argument_group("Global") - group_global.add_argument('-d', '--device', required=False, type=str, metavar="", help="Specify the target Android device ID for ADB connection.") - group_global.add_argument('-v', '--verbose', required=False, action="store_true", help="Enable verbose logging for detailed debug output.") - group_global.add_argument('-l', '--log', required=False, type=Path, metavar="", help="Directory to store log files.") - group_global.add_argument('--delay', required=False, type=float, metavar="", default=1, help="Delay (in seconds) between process checks.") - group_global.add_argument('--version', required=False, action="store_true", help="Display KeyDive version information.") + cdm_group = parser.add_argument_group('CDM Extraction') + cdm_group.add_argument('-o', '--output', type=Path, default='device', metavar='', help='Directory to store extracted CDM files. (default: ./device)') + cdm_group.add_argument('-w', '--wvd', action='store_true', help='Export data in pywidevine-compatible WVD format.') + cdm_group.add_argument('-k', '--keybox', action='store_true', help='Export Widevine keybox if available on the device.') + cdm_group.add_argument('-a', '--auto', choices=['web', 'player'], metavar='', help='Automatically launch a DRM playback test. ("web" or "player")') - # Arguments specific to the CDM (Content Decryption Module) - group_cdm = parser.add_argument_group("Cdm") - group_cdm.add_argument('-o', '--output', required=False, type=Path, default=Path("device"), metavar="", help="Output directory for extracted data.") - group_cdm.add_argument('-w', '--wvd', required=False, action="store_true", help="Generate a pywidevine WVD device file.") - group_cdm.add_argument('-s', '--skip', required=False, action="store_true", help="Skip auto-detection of the private function.") - group_cdm.add_argument('-a', '--auto', required=False, action="store_true", help="Automatically start the Bitmovin web player.") - group_cdm.add_argument('-p', '--player', required=False, action="store_true", help="Install and start the Kaltura app automatically.") - - # Advanced options - group_advanced = parser.add_argument_group("Advanced") - group_advanced.add_argument('-f', '--functions', required=False, type=Path, metavar="", help="Path to Ghidra XML functions file.") - group_advanced.add_argument('-k', '--keybox', required=False, action="store_true", help="Enable export of the Keybox data if it is available.") - group_advanced.add_argument('--challenge', required=False, type=Path, metavar="", help="Path to unencrypted challenge for extracting client ID.") - group_advanced.add_argument('--private-key', required=False, type=Path, metavar="", help="Path to private key for extracting client ID.") + advanced_group = parser.add_argument_group('Advanced Options') + advanced_group.add_argument('--no-detect', action='store_false', help='Disable automatic detection of OEM private key function.') + advanced_group.add_argument('--no-disabler', action='store_false', help='Disable liboemcrypto-disabler module (patches memory protection).') + advanced_group.add_argument('--no-stop', action='store_false', help='Do not stop once minimum CDM data is intercepted.') + advanced_group.add_argument('--unencrypt', action='store_true', help='Force the license challenge to keep client ID data unencrypted.') + advanced_group.add_argument('--symbols', type=Path, metavar='', help='Path to Ghidra-generated XML symbol file for function mapping.') + advanced_group.add_argument('--challenge', action='append', type=Path, metavar='', help='Protobuf challenge file(s) captured via MITM proxy.') + advanced_group.add_argument('--rsa-key', action='append', type=Path, metavar='', help='RSA private key(s) in PEM or DER format for client ID decryption.') + advanced_group.add_argument('--aes-key', action='append', type=str, metavar='', help='AES key(s) in hex, base64, or file form for decrypting keybox data.') + # Parse command-line arguments args = parser.parse_args() - # Handle version display + # Handle version flag early and exit if args.version: - print(f"KeyDive {keydive.__version__}") - exit(0) + print(f'KeyDive {keydive.__version__}') + return - # Configure logging (file and console) + # Set up logging (to file if specified, otherwise stdout) log_path = configure_logging(path=args.log, verbose=args.verbose) - logger = logging.getLogger("KeyDive") - logger.info("Version: %s", keydive.__version__) + logger = logging.getLogger('keydive') + logger.info('Version: %s', keydive.__version__) try: - # Connect to the specified Android device - adb = ADB(device=args.device) + # Initialize core logic with device serial, Ghidra symbols, and feature toggles + core = Core( + serial=args.serial, + symbols=args.symbols, + detect=args.no_detect, + disabler=args.no_disabler, + unencrypt=args.unencrypt + ) - # Initialize Cdm instance for content decryption module (with optional arguments) - cdm = Cdm(keybox=args.keybox) - if args.challenge: - cdm.set_challenge(data=args.challenge) - if args.private_key: - cdm.set_private_key(data=args.private_key, name=None) + # Provide challenge files if intercepted (optional, used for verification/analysis) + core.cdm.set_challenge(data=args.challenge) - # Initialize Core instance for interacting with the device - core = Core(adb=adb, cdm=cdm, functions=args.functions, skip=args.skip) + # Provide RSA private keys (PEM or DER format) for client ID decryption + core.cdm.set_private_key(data=args.rsa_key, name=None) - # Setup actions based on user arguments (for DRM player, Bitmovin player, etc.) - if args.player: - package = DRM_PLAYER["package"] + # Provide AES key(s) for decrypting the keybox or related data + core.cdm.set_device_aes_key(data=args.aes_key) - # Check if the application is already installed - installed = package in adb.list_applications(user=True, system=False) - if not installed: - logger.debug("Application %s not found. Installing...", package) - installed = adb.install_application(path=DRM_PLAYER["path"], url=DRM_PLAYER["url"]) + # Optionally launch a DRM playback test (web-based or player-based) + if args.auto: + core.launch(action=args.auto) - # Skip starting the application if installation failed - if installed: - # Start the application - logger.info("Starting application: %s", package) - adb.start_application(package) - elif args.auto: - logger.info("Opening the Bitmovin web player...") - adb.open_url("https://bitmovin.com/demos/drm") - logger.info("Setup completed") - - # Process watcher loop: continuously checks for Widevine processes - logger.info("Watcher delay: %ss" % args.delay) - current = None # Variable to track the current Widevine process - while core.running: - # Check if for current process data has been exported - if current and cdm.export(args.output, args.wvd): - raise KeyboardInterrupt # Stop if export is complete - - # Get the currently running Widevine processes - # https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792 - processes = { - key: (name, pid) - for name, pid in adb.enumerate_processes().items() - for key in CDM_VENDOR_API.keys() - if key in name or key.replace("-service", "-service-lazy") 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 and current not in [v[1] for v in processes.values()]: - logger.warning("Widevine process has changed") - current = None - - # 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") - else: - logger.warning("Widevine library not found, searching...") - - # Delay before next iteration - time.sleep(args.delay) + # Begin monitoring the device to extract Widevine CDM data + # Stops automatically if sufficient data is collected, unless --no-stop is specified + core.watchdog(output=args.output, delay=args.delay, auto_stop=args.no_stop, wvd=args.wvd, keybox=args.keybox) except KeyboardInterrupt: + # Graceful exit on user interrupt (Ctrl+C) pass except Exception as e: + # Log any unhandled exception, include traceback if verbose logging is enabled logger.critical(e, exc_info=args.verbose) - # Final logging and exit + # Log the path to the log file if it was created if log_path: - logger.info("Log file: %s" % log_path) - logger.info("Exiting") + logger.info('Log file: %s' % log_path) + + logger.info('Exiting') -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/keydive/adb.py b/keydive/adb.py deleted file mode 100644 index 725d0f3..0000000 --- a/keydive/adb.py +++ /dev/null @@ -1,287 +0,0 @@ -import logging -import re -import shutil -import subprocess - -from pathlib import Path - -import frida -import requests - -from frida.core import Device - -# Suppress urllib3 warnings -logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR) - - -def shell(prompt: list) -> subprocess.CompletedProcess: - """ - Executes a shell command and returns the result. - - Parameters: - prompt (list): The command to execute as a list of strings. - - Returns: - subprocess.CompletedProcess: The result containing return code, stdout, and stderr. - """ - prompt = list(map(str, prompt)) # Ensure all command parts are strings - # logging.getLogger("Shell").debug(" ".join(prompt)) - return subprocess.run(prompt, capture_output=True) # Run the command and capture output - - -class ADB: - """ - Class for managing interactions with the Android device via ADB. - """ - - def __init__(self, device: str = None, timeout: int = 5): - """ - Initializes ADB connection to the device. - - Parameters: - device (str, optional): Device ID to connect to, defaults to the first USB device. - timeout (int, optional): Timeout for connection in seconds. Defaults to 5. - - Raises: - EnvironmentError: If ADB is not found in the system path. - Exception: If connection to the device fails. - """ - self.logger = logging.getLogger(self.__class__.__name__) - - # Ensure ADB is available - if not shutil.which("adb"): - raise EnvironmentError( - "ADB is not recognized as an environment variable. " - "Ensure ADB is installed and refer to the documentation: " - "https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge" - ) - - # Start the ADB server if not already running - sp = shell(['adb', 'start-server']) - if sp.returncode != 0: - self.logger.warning("ADB server startup failed (Error: %s)", sp.stdout.decode("utf-8").strip()) - - # Connect to device (or default to the first USB device) - try: - self.device: Device = frida.get_device(id=device, timeout=timeout) if device else frida.get_usb_device(timeout=timeout) - self.logger.info("Connected to device: %s (%s)", self.device.name, self.device.id) - except Exception as e: - self.logger.error("Failed to connect to device: %s", e) - raise e - - self.prompt = ['adb', '-s', self.device.id, 'shell'] - - # Retrieve and log device properties - properties = self.device_properties() - if properties: - self.logger.info("SDK API: %s", properties.get("ro.build.version.sdk", "Unknown")) - self.logger.info("ABI CPU: %s", properties.get("ro.product.cpu.abi", "Unknown")) - else: - self.logger.warning("No device properties retrieved") - - def device_properties(self) -> dict: - """ - Retrieves system properties from the device. - - Returns: - dict: A dictionary mapping property keys to their corresponding values. - """ - # https://source.android.com/docs/core/architecture/configuration/add-system-properties?#shell-commands - properties = {} - - # Execute the shell command to retrieve device properties - sp = shell([*self.prompt, 'getprop']) - if sp.returncode != 0: - self.logger.error("Failed to retrieve device properties (Error: %s)", sp.stdout.decode("utf-8").strip()) - return properties - - # Parse the output and cast values accordingly - for line in sp.stdout.decode("utf-8").splitlines(): - match = re.match(r"\[(.*?)\]: \[(.*?)\]", line) - if match: - key, value = match.groups() - - # Cast numeric and boolean values where appropriate - if value.isdigit(): - value = int(value) - elif value.lower() in ("true", "false"): - value = value.lower() == "true" - - properties[key] = value - - return properties - - def list_applications(self, user: bool = True, system: bool = False) -> dict: - """ - Lists installed applications on the device, with optional filters for user/system apps. - - Parameters: - user (bool, optional): Include user-installed apps. Defaults to True. - system (bool, optional): Include system apps. Defaults to False. - - Returns: - dict: A dictionary of application packages and their file paths. - """ - applications = {} - - # Validate input; return empty dict if no filter is set - if not user and not system: - return applications - - # Set the appropriate shell command based on user/system filters - prompt = [*self.prompt, 'pm', 'list', 'packages', '-f'] - if user and not system: - prompt.append("-3") - elif not user and system: - prompt.append("-s") - - # Execute the shell command to list applications - sp = shell(prompt) - if sp.returncode != 0: - self.logger.error("Failed to retrieve app list (Error: %s)", sp.stdout.decode("utf-8").strip()) - return applications - - # Parse and add applications to the dictionary - for line in sp.stdout.decode("utf-8").splitlines(): - try: - path, package = line.strip().split(":", 1)[1].rsplit("=", 1) - applications[package] = path - except Exception as e: - pass - - return applications - - def start_application(self, package: str) -> bool: - """ - Starts an application by its package name. - - Parameters: - package (str): The package name of the application. - - Returns: - bool: True if the app was started successfully, False otherwise. - """ - # Get package information using dumpsys - sp = shell([*self.prompt, 'dumpsys', 'package', package]) - lines = sp.stdout.decode("utf-8").splitlines() - - # Remove empty lines to ensure backwards compatibility - lines = [l.strip() for l in lines if l.strip()] - - # Look for MAIN activity to identify entry point - for i, line in enumerate(lines): - if "android.intent.action.MAIN" in line: - match = re.search(fr"({package}/[^ ]+)", lines[i + 1]) - if match: - # Start the application by its main activity - main_activity = match.group() - sp = shell([*self.prompt, 'am', 'start', '-n', main_activity]) - if sp.returncode == 0: - return True - - self.logger.error("Failed to start app %s (Error: %s)", package, sp.stdout.decode("utf-8").strip()) - break - - self.logger.error("Package %s not found or no MAIN intent", package) - return False - - def enumerate_processes(self) -> dict: - """ - Lists running processes and maps process names to their PIDs. - - Returns: - dict: Dictionary of process names and corresponding PIDs. - """ - # https://github.com/frida/frida/issues/1225#issuecomment-604181822 - processes = {} - - # Attempt to get the list of processes using the 'ps -A' command - prompt = [*self.prompt, 'ps'] - sp = shell([*prompt, '-A']) - lines = sp.stdout.decode("utf-8").splitlines() - - # If the output has less than 10 lines, retry with a simpler 'ps' command - if len(lines) < 10: - sp = shell(prompt) - if sp.returncode != 0: - self.logger.error("Failed to execute ps command (Error: %s)", sp.stdout.decode("utf-8").strip()) - return processes - lines = sp.stdout.decode("utf-8").splitlines() - - # Iterate through lines starting from the second line (skipping header) - for line in lines[1:]: - try: - parts = line.split() # USER,PID,PPID,VSZ,RSS,WCHAN,ADDR,S,NAME - pid = int(parts[1]) # Extract PID - name = " ".join(parts[8:]).strip() # Extract process name - - # Handle cases where process name might be in brackets (e.g., kernel threads) - name = name if name.startswith("[") else Path(name).name - processes[name] = pid - except Exception as e: - pass - - return processes - - def install_application(self, path: Path = None, url: str = None) -> bool: - """ - Installs an application on the device either from a local file or by downloading from a URL. - - Parameters: - path (Path, optional): The local file path of the APK to install. Defaults to None. - url (str, optional): The URL to download the APK from. Defaults to None. - - Returns: - bool: True if the installation was successful, False otherwise. - """ - # Prepare the shell command for installation - prompt = [*self.prompt[:-1], 'install'] - - # Install from a local file path if a valid path is provided - if path and path.is_file(): - sp = shell([*prompt, path]) # Run the installation command with the local file path - if sp.returncode == 0: - return True - self.logger.error("Installation failed for local path: %s (Error: %s)", path, sp.stdout.decode("utf-8").strip()) - - # If URL is provided, attempt to download the APK and install it - status = False - if url: - file = Path("tmp.apk") # Temporary file to store the downloaded APK - try: - # Download the APK from the provided URL - r = requests.get(url, headers={"Accept": "*/*", "User-Agent": "KeyDive/ADB"}) - r.raise_for_status() - - # Save the downloaded APK to a temporary file - file.write_bytes(r.content) - - # Attempt installation from the downloaded APK - status = self.install_application(path=file) - except Exception as e: - self.logger.error("Failed to download application from URL: %s (Error: %s)", url, e) - file.unlink(missing_ok=True) # Clean up the temporary file, even if there was an error - - return status - - def open_url(self, url: str) -> bool: - """ - Opens a specified URL on the device. - - Parameters: - url (str): The URL to be opened on the device. - - Returns: - bool: True if the URL was successfully opened, False otherwise. - """ - # Execute the shell command to open the URL using the Android 'am' (Activity Manager) command. - sp = shell([*self.prompt, 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', url]) - - # Check the result of the command execution and log if there is an error - if sp.returncode != 0: - self.logger.error("URL open failed for: %s (Return: %s)", url, sp.stdout.decode("utf-8").strip()) - return False - return True - - -__all__ = ("ADB",) diff --git a/keydive/adb/__init__.py b/keydive/adb/__init__.py new file mode 100644 index 0000000..d957d3c --- /dev/null +++ b/keydive/adb/__init__.py @@ -0,0 +1,171 @@ +from pathlib import Path + +# https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:bionic/libc/libc.map.txt +NATIVE_C_API = ( + '__assert', '__assert2', '__atomic_cmpxchg', '__atomic_dec', '__atomic_inc', '__atomic_swap', '__b64_ntop', + '__b64_pton', '__cmsg_nxthdr', '__connect', '__ctype_get_mb_cur_max', '__cxa_atexit', '__cxa_finalize', + '__cxa_thread_atexit_impl', '__dn_comp', '__dn_count_labels', '__dn_skipname', '__epoll_pwait', '__errno', '__exit', + '__fadvise64', '__fbufsize', '__fcntl64', '__FD_CLR_chk', '__FD_ISSET_chk', '__FD_SET_chk', '__fgets_chk', '__flbf', + '__fp_nquery', '__fp_query', '__fpclassify', '__fpclassifyd', '__fpclassifyf', '__fpclassifyl', '__fpending', + '__fpurge', '__freadable', '__fsetlocking', '__fstatfs64', '__fwritable', '__get_h_errno', '__getcpu', '__getcwd', + '__getpid', '__getpriority', '__gnu_basename', '__gnu_strerror_r', '__hostalias', '__ioctl', '__isfinite', + '__isfinitef', '__isfinitel', '__isinf', '__isinff', '__isinfl', '__isnan', '__isnanf', '__isnanl', '__isnormal', + '__isnormalf', '__isnormall', '__isthreaded', '__libc_current_sigrtmax', '__libc_current_sigrtmin', '__libc_init', + '__llseek', '__loc_aton', '__loc_ntoa', '__memchr_chk', '__memcpy_chk', '__memmove_chk', '__memrchr_chk', + '__memset_chk', '__mmap2', '__ns_format_ttl', '__ns_get16', '__ns_get32', '__ns_initparse', '__ns_makecanon', + '__ns_msg_getflag', '__ns_name_compress', '__ns_name_ntol', '__ns_name_ntop', '__ns_name_pack', '__ns_name_pton', + '__ns_name_rollback', '__ns_name_skip', '__ns_name_uncompress', '__ns_name_unpack', '__ns_parserr', '__ns_put16', + '__ns_put32', '__ns_samename', '__ns_skiprr', '__ns_sprintrr', '__ns_sprintrrf', '__open_2', '__openat', + '__openat_2', '__p_cdname', '__p_cdnname', '__p_class', '__p_class_syms', '__p_fqname', '__p_fqnname', '__p_option', + '__p_query', '__p_rcode', '__p_secstodate', '__p_time', '__p_type', '__p_type_syms', '__poll_chk', '__ppoll', + '__ppoll_chk', '__ppoll64_chk', '__pread64_chk', '__pread_chk', '__progname', '__pselect6', '__pthread_cleanup_pop', + '__pthread_cleanup_push', '__ptrace', '__putlong', '__putshort', '__read_chk', '__readlink_chk', '__readlinkat_chk', + '__reboot', '__recvfrom_chk', '__register_atfork', '__res_close', '__res_dnok', '__res_hnok', '__res_hostalias', + '__res_isourserver', '__res_mailok', '__res_nameinquery', '__res_nclose', '__res_ninit', '__res_nmkquery', + '__res_nquery', '__res_nquerydomain', '__res_nsearch', '__res_nsend', '__res_ownok', '__res_queriesmatch', + '__res_querydomain', '__res_send', '__res_send_setqhook', '__res_send_setrhook', '__rt_sigaction', + '__rt_sigpending', '__rt_sigprocmask', '__rt_sigsuspend', '__rt_sigtimedwait', '__sched_cpualloc', + '__sched_cpucount', '__sched_cpufree', '__sched_getaffinity', '__set_thread_area', '__set_tid_address', '__set_tls', + '__sF', '__sigaction', '__snprintf_chk', '__socket', '__sprintf_chk', '__stack_chk_fail', '__stack_chk_guard', + '__statfs64', '__stpcpy_chk', '__stpncpy_chk', '__stpncpy_chk2', '__strcat_chk', '__strchr_chk', '__strcpy_chk', + '__strlcat_chk', '__strlcpy_chk', '__strlen_chk', '__strncat_chk', '__strncpy_chk', '__strncpy_chk2', + '__strrchr_chk', '__sym_ntop', '__sym_ntos', '__sym_ston', '__system_property_area_serial', + '__system_property_find', '__system_property_find_nth', '__system_property_foreach', '__system_property_get', + '__system_property_read', '__system_property_serial', '__system_property_set', '__timer_create', '__timer_delete', + '__timer_getoverrun', '__timer_gettime', '__timer_settime', '__umask_chk', '__vsnprintf_chk', '__vsprintf_chk', + '__waitid', '_ctype_', '_Exit', '_exit', '_flushlbf', '_getlong', '_getshort', '_longjmp', + '_resolv_delete_cache_for_net', '_resolv_flush_cache_for_net', '_resolv_set_nameservers_for_net', '_setjmp', + '_tolower', '_tolower_tab_', '_toupper', '_toupper_tab_', 'abort', 'abs', 'accept', 'accept4', 'access', 'acct', + 'alarm', 'alphasort', 'alphasort64', 'android_set_abort_message', 'arc4random', 'arc4random_buf', + 'arc4random_uniform', 'asctime', 'asctime64', 'asctime64_r', 'asctime_r', 'asprintf', 'at_quick_exit', 'atof', + 'atoi', 'atol', 'atoll', 'basename', 'basename_r', 'bind', 'bindresvport', 'brk', 'bsearch', 'btowc', 'c16rtomb', + 'c32rtomb', 'cacheflush', 'calloc', 'capget', 'capset', 'cfgetispeed', 'cfgetospeed', 'cfmakeraw', 'cfsetispeed', + 'cfsetospeed', 'cfsetspeed', 'chdir', 'chmod', 'chown', 'chroot', 'clearenv', 'clearerr', 'clearerr_unlocked', + 'clock', 'clock_getcpuclockid', 'clock_getres', 'clock_gettime', 'clock_nanosleep', 'clock_settime', 'clone', + 'close', 'closedir', 'closelog', 'connect', 'creat', 'creat64', 'ctime', 'ctime64', 'ctime64_r', 'ctime_r', + 'daemon', 'daylight', 'delete_module', 'difftime', 'dirfd', 'dirname', 'dirname_r', 'div', 'dn_expand', 'dprintf', + 'drand48', 'dup', 'dup2', 'dup3', 'duplocale', 'endmntent', 'endservent', 'endutent', 'environ', 'epoll_create', + 'epoll_create1', 'epoll_ctl', 'epoll_pwait', 'epoll_wait', 'erand48', 'err', 'error', 'error_at_line', + 'error_message_count', 'error_one_per_line', 'error_print_progname', 'errx', 'ether_aton', 'ether_aton_r', + 'ether_ntoa', 'ether_ntoa_r', 'eventfd', 'eventfd_read', 'eventfd_write', 'execl', 'execle', 'execlp', 'execv', + 'execve', 'execvp', 'execvpe', 'exit', 'faccessat', 'fallocate', 'fallocate64', 'fchdir', 'fchmod', 'fchmodat', + 'fchown', 'fchownat', 'fclose', 'fcntl', 'fdatasync', 'fdopen', 'fdopendir', 'fdprintf', 'feof', 'feof_unlocked', + 'ferror', 'ferror_unlocked', 'fflush', 'ffs', 'fgetc', 'fgetln', 'fgetpos', 'fgets', 'fgetwc', 'fgetws', + 'fgetxattr', 'fileno', 'flistxattr', 'flock', 'flockfile', 'fmemopen', 'fnmatch', 'fopen', 'fork', 'forkpty', + 'fpathconf', 'fprintf', 'fpurge', 'fputc', 'fputs', 'fputwc', 'fputws', 'fread', 'free', 'freeaddrinfo', + 'freelocale', 'fremovexattr', 'freopen', 'fscanf', 'fseek', 'fseeko', 'fsetpos', 'fsetxattr', 'fstat', 'fstat64', + 'fstatat', 'fstatat64', 'fstatfs', 'fstatfs64', 'fstatvfs', 'fstatvfs64', 'fsync', 'ftell', 'ftello', 'ftok', + 'ftruncate', 'ftruncate64', 'ftrylockfile', 'fts_children', 'fts_close', 'fts_open', 'fts_read', 'fts_set', 'ftw', + 'ftw64', 'funlockfile', 'funopen', 'futimens', 'fwide', 'fwprintf', 'fwrite', 'fwscanf', 'gai_strerror', + 'get_avphys_pages', 'get_nprocs', 'get_nprocs_conf', 'get_phys_pages', 'getaddrinfo', 'getauxval', 'getc', + 'getc_unlocked', 'getchar', 'getchar_unlocked', 'getcwd', 'getdelim', 'getegid', 'getenv', 'geteuid', 'getgid', + 'getgrgid', 'getgrnam', 'getgrouplist', 'getgroups', 'gethostbyaddr', 'gethostbyaddr_r', 'gethostbyname', + 'gethostbyname2', 'gethostbyname2_r', 'gethostbyname_r', 'gethostent', 'gethostname', 'getitimer', 'getline', + 'getlogin', 'getmntent', 'getmntent_r', 'getnameinfo', 'getnetbyaddr', 'getnetbyname', 'getopt', 'getopt_long', + 'getopt_long_only', 'getpagesize', 'getpeername', 'getpgid', 'getpgrp', 'getpid', 'getppid', 'getpriority', + 'getprogname', 'getprotobyname', 'getprotobynumber', 'getpt', 'getpwnam', 'getpwnam_r', 'getpwuid', 'getpwuid_r', + 'getresgid', 'getresuid', 'getrlimit', 'getrlimit64', 'getrusage', 'gets', 'getservbyname', 'getservbyport', + 'getservent', 'getsid', 'getsockname', 'getsockopt', 'gettid', 'gettimeofday', 'getuid', 'getutent', 'getwc', + 'getwchar', 'getxattr', 'gmtime', 'gmtime64', 'gmtime64_r', 'gmtime_r', 'grantpt', 'herror', 'hstrerror', 'htonl', + 'htons', 'if_indextoname', 'if_nametoindex', 'imaxabs', 'imaxdiv', 'inet_addr', 'inet_aton', 'inet_lnaof', + 'inet_makeaddr', 'inet_netof', 'inet_network', 'inet_nsap_addr', 'inet_nsap_ntoa', 'inet_ntoa', 'inet_ntop', + 'inet_pton', 'init_module', 'initgroups', 'initstate', 'inotify_add_watch', 'inotify_init', 'inotify_init1', + 'inotify_rm_watch', 'insque', 'ioctl', 'isalnum', 'isalnum_l', 'isalpha', 'isalpha_l', 'isascii', 'isatty', + 'isblank', 'isblank_l', 'iscntrl', 'iscntrl_l', 'isdigit', 'isdigit_l', 'isfinite', 'isfinitef', 'isfinitel', + 'isgraph', 'isgraph_l', 'isinf', 'isinff', 'isinfl', 'islower', 'islower_l', 'isnan', 'isnanf', 'isnanl', + 'isnormal', 'isnormalf', 'isnormall', 'isprint', 'isprint_l', 'ispunct', 'ispunct_l', 'isspace', 'isspace_l', + 'isupper', 'isupper_l', 'iswalnum', 'iswalnum_l', 'iswalpha', 'iswalpha_l', 'iswblank', 'iswblank_l', 'iswcntrl', + 'iswcntrl_l', 'iswctype', 'iswctype_l', 'iswdigit', 'iswdigit_l', 'iswgraph', 'iswgraph_l', 'iswlower', + 'iswlower_l', 'iswprint', 'iswprint_l', 'iswpunct', 'iswpunct_l', 'iswspace', 'iswspace_l', 'iswupper', + 'iswupper_l', 'iswxdigit', 'iswxdigit_l', 'isxdigit', 'isxdigit_l', 'jrand48', 'kill', 'killpg', 'klogctl', 'labs', + 'lchown', 'lcong48', 'ldexp', 'ldiv', 'lfind', 'lgetxattr', 'link', 'linkat', 'listen', 'listxattr', 'llabs', + 'lldiv', 'llistxattr', 'localeconv', 'localtime', 'localtime64', 'localtime64_r', 'localtime_r', 'login_tty', + 'longjmp', 'lrand48', 'lremovexattr', 'lsearch', 'lseek', 'lseek64', 'lsetxattr', 'lstat', 'lstat64', 'madvise', + 'mallinfo', 'malloc', 'malloc_info', 'malloc_usable_size', 'mbrlen', 'mbrtoc16', 'mbrtoc32', 'mbrtowc', 'mbsinit', + 'mbsnrtowcs', 'mbsrtowcs', 'mbstowcs', 'mbtowc', 'memalign', 'memccpy', 'memchr', 'memcmp', 'memcpy', 'memmem', + 'memmove', 'mempcpy', 'memrchr', 'memset', 'mincore', 'mkdir', 'mkdirat', 'mkdtemp', 'mkfifo', 'mkfifoat', 'mknod', + 'mknodat', 'mkostemp', 'mkostemp64', 'mkostemps', 'mkostemps64', 'mkstemp', 'mkstemp64', 'mkstemps', 'mkstemps64', + 'mktemp', 'mktime', 'mktime64', 'mlock', 'mlockall', 'mmap', 'mmap64', 'mount', 'mprotect', 'mrand48', 'mremap', + 'msync', 'munlock', 'munlockall', 'munmap', 'nanosleep', 'newlocale', 'nftw', 'nftw64', 'nice', 'nrand48', + 'ns_format_ttl', 'ns_get16', 'ns_get32', 'ns_initparse', 'ns_makecanon', 'ns_msg_getflag', 'ns_name_compress', + 'ns_name_ntol', 'ns_name_ntop', 'ns_name_pack', 'ns_name_pton', 'ns_name_rollback', 'ns_name_skip', + 'ns_name_uncompress', 'ns_name_unpack', 'ns_parserr', 'ns_put16', 'ns_put32', 'ns_samename', 'ns_skiprr', + 'ns_sprintrr', 'ns_sprintrrf', 'nsdispatch', 'ntohl', 'ntohs', 'open', 'open64', 'open_memstream', + 'open_wmemstream', 'openat', 'openat64', 'opendir', 'openlog', 'openpty', 'optarg', 'opterr', 'optind', 'optopt', + 'optreset', 'pathconf', 'pause', 'pclose', 'perror', 'personality', 'pipe', 'pipe2', 'poll', 'popen', + 'posix_fadvise', 'posix_fadvise64', 'posix_fallocate', 'posix_fallocate64', 'posix_madvise', 'posix_memalign', + 'posix_openpt', 'ppoll', 'prctl', 'pread', 'pread64', 'printf', 'prlimit', 'prlimit64', 'process_vm_readv', + 'process_vm_writev', 'pselect', 'psiginfo', 'psignal', 'pthread_atfork', 'pthread_attr_destroy', + 'pthread_attr_getdetachstate', 'pthread_attr_getguardsize', 'pthread_attr_getschedparam', + 'pthread_attr_getschedpolicy', 'pthread_attr_getscope', 'pthread_attr_getstack', 'pthread_attr_getstacksize', + 'pthread_attr_init', 'pthread_attr_setdetachstate', 'pthread_attr_setguardsize', 'pthread_attr_setschedparam', + 'pthread_attr_setschedpolicy', 'pthread_attr_setscope', 'pthread_attr_setstack', 'pthread_attr_setstacksize', + 'pthread_cond_broadcast', 'pthread_cond_destroy', 'pthread_cond_init', 'pthread_cond_signal', + 'pthread_cond_timedwait', 'pthread_cond_timedwait_monotonic', 'pthread_cond_timedwait_monotonic_np', + 'pthread_cond_timedwait_relative_np', 'pthread_cond_timeout_np', 'pthread_cond_wait', 'pthread_condattr_destroy', + 'pthread_condattr_getclock', 'pthread_condattr_getpshared', 'pthread_condattr_init', 'pthread_condattr_setclock', + 'pthread_condattr_setpshared', 'pthread_create', 'pthread_detach', 'pthread_equal', 'pthread_exit', + 'pthread_getattr_np', 'pthread_getcpuclockid', 'pthread_getschedparam', 'pthread_getspecific', 'pthread_gettid_np', + 'pthread_join', 'pthread_key_create', 'pthread_key_delete', 'pthread_kill', 'pthread_mutex_destroy', + 'pthread_mutex_init', 'pthread_mutex_lock', 'pthread_mutex_lock_timeout_np', 'pthread_mutex_timedlock', + 'pthread_mutex_trylock', 'pthread_mutex_unlock', 'pthread_mutexattr_destroy', 'pthread_mutexattr_getpshared', + 'pthread_mutexattr_gettype', 'pthread_mutexattr_init', 'pthread_mutexattr_setpshared', 'pthread_mutexattr_settype', + 'pthread_once', 'pthread_rwlock_destroy', 'pthread_rwlock_init', 'pthread_rwlock_rdlock', + 'pthread_rwlock_timedrdlock', 'pthread_rwlock_timedwrlock', 'pthread_rwlock_tryrdlock', 'pthread_rwlock_trywrlock', + 'pthread_rwlock_unlock', 'pthread_rwlock_wrlock', 'pthread_rwlockattr_destroy', 'pthread_rwlockattr_getkind_np', + 'pthread_rwlockattr_getpshared', 'pthread_rwlockattr_init', 'pthread_rwlockattr_setkind_np', + 'pthread_rwlockattr_setpshared', 'pthread_self', 'pthread_setname_np', 'pthread_setschedparam', + 'pthread_setspecific', 'pthread_sigmask', 'ptrace', 'ptsname', 'ptsname_r', 'putc', 'putc_unlocked', 'putchar', + 'putchar_unlocked', 'putenv', 'puts', 'pututline', 'putw', 'putwc', 'putwchar', 'pvalloc', 'pwrite', 'pwrite64', + 'qsort', 'quick_exit', 'raise', 'rand', 'rand_r', 'random', 'read', 'readahead', 'readdir', 'readdir64', + 'readdir64_r', 'readdir_r', 'readlink', 'readlinkat', 'readv', 'realloc', 'realpath', 'reboot', 'recv', 'recvfrom', + 'recvmmsg', 'recvmsg', 'regcomp', 'regerror', 'regexec', 'regfree', 'remove', 'removexattr', 'remque', 'rename', + 'renameat', 'res_init', 'res_mkquery', 'res_query', 'res_search', 'rewind', 'rewinddir', 'rmdir', 'sbrk', 'scandir', + 'scandir64', 'scanf', 'sched_get_priority_max', 'sched_get_priority_min', 'sched_getaffinity', 'sched_getcpu', + 'sched_getparam', 'sched_getscheduler', 'sched_rr_get_interval', 'sched_setaffinity', 'sched_setparam', + 'sched_setscheduler', 'sched_yield', 'seed48', 'seekdir', 'select', 'sem_close', 'sem_destroy', 'sem_getvalue', + 'sem_init', 'sem_open', 'sem_post', 'sem_timedwait', 'sem_trywait', 'sem_unlink', 'sem_wait', 'send', 'sendfile', + 'sendfile64', 'sendmmsg', 'sendmsg', 'sendto', 'setbuf', 'setbuffer', 'setegid', 'setenv', 'seteuid', 'setfsgid', + 'setfsuid', 'setgid', 'setgroups', 'sethostname', 'setitimer', 'setjmp', 'setlinebuf', 'setlocale', 'setlogmask', + 'setmntent', 'setns', 'setpgid', 'setpgrp', 'setpriority', 'setprogname', 'setregid', 'setresgid', 'setresuid', + 'setreuid', 'setrlimit', 'setrlimit64', 'setservent', 'setsid', 'setsockopt', 'setstate', 'settimeofday', 'setuid', + 'setutent', 'setvbuf', 'setxattr', 'shutdown', 'sigaction', 'sigaddset', 'sigaltstack', 'sigblock', 'sigdelset', + 'sigemptyset', 'sigfillset', 'siginterrupt', 'sigismember', 'siglongjmp', 'signal', 'signalfd', 'sigpending', + 'sigprocmask', 'sigqueue', 'sigsetjmp', 'sigsetmask', 'sigsuspend', 'sigtimedwait', 'sigwait', 'sigwaitinfo', + 'sleep', 'snprintf', 'socket', 'socketpair', 'splice', 'sprintf', 'srand', 'srand48', 'srandom', 'sscanf', 'stat', + 'stat64', 'statfs', 'statfs64', 'statvfs', 'statvfs64', 'stderr', 'stdin', 'stdout', 'stpcpy', 'stpncpy', + 'strcasecmp', 'strcasecmp_l', 'strcasestr', 'strcat', 'strchr', 'strcmp', 'strcoll', 'strcoll_l', 'strcpy', + 'strcspn', 'strdup', 'strerror', 'strerror_l', 'strerror_r', 'strftime', 'strftime_l', 'strlcat', 'strlcpy', + 'strlen', 'strncasecmp', 'strncasecmp_l', 'strncat', 'strncmp', 'strncpy', 'strndup', 'strnlen', 'strpbrk', + 'strptime', 'strrchr', 'strsep', 'strsignal', 'strspn', 'strstr', 'strtod', 'strtof', 'strtoimax', 'strtok', + 'strtok_r', 'strtol', 'strtold', 'strtold_l', 'strtoll', 'strtoll_l', 'strtoul', 'strtoull', 'strtoull_l', + 'strtoumax', 'strxfrm', 'strxfrm_l', 'swapoff', 'swapon', 'swprintf', 'swscanf', 'symlink', 'symlinkat', 'sync', + 'sys_siglist', 'sys_signame', 'syscall', 'sysconf', 'sysinfo', 'syslog', 'system', 'tcdrain', 'tcflow', 'tcflush', + 'tcgetattr', 'tcgetpgrp', 'tcgetsid', 'tcsendbreak', 'tcsetattr', 'tcsetpgrp', 'tdelete', 'tdestroy', 'tee', + 'telldir', 'tempnam', 'tfind', 'tgkill', 'time', 'timegm', 'timegm64', 'timelocal', 'timelocal64', 'timer_create', + 'timer_delete', 'timer_getoverrun', 'timer_gettime', 'timer_settime', 'timerfd_create', 'timerfd_gettime', + 'timerfd_settime', 'times', 'timezone', 'tmpfile', 'tmpnam', 'toascii', 'tolower', 'tolower_l', 'toupper', + 'toupper_l', 'towlower', 'towlower_l', 'towupper', 'towupper_l', 'truncate', 'truncate64', 'tsearch', 'ttyname', + 'ttyname_r', 'twalk', 'tzname', 'tzset', 'umask', 'umount', 'umount2', 'uname', 'ungetc', 'ungetwc', 'unlink', + 'unlinkat', 'unlockpt', 'unsetenv', 'unshare', 'uselocale', 'usleep', 'utime', 'utimensat', 'utimes', 'utmpname', + 'valloc', 'vasprintf', 'vdprintf', 'verr', 'verrx', 'vfdprintf', 'vfork', 'vfprintf', 'vfscanf', 'vfwprintf', + 'vfwscanf', 'vmsplice', 'vprintf', 'vscanf', 'vsnprintf', 'vsprintf', 'vsscanf', 'vswprintf', 'vswscanf', 'vsyslog', + 'vwarn', 'vwarnx', 'vwprintf', 'vwscanf', 'wait', 'wait4', 'waitid', 'waitpid', 'warn', 'warnx', 'wcpcpy', + 'wcpncpy', 'wcrtomb', 'wcscasecmp', 'wcscasecmp_l', 'wcscat', 'wcschr', 'wcscmp', 'wcscoll', 'wcscoll_l', 'wcscpy', + 'wcscspn', 'wcsdup', 'wcsftime', 'wcslcat', 'wcslcpy', 'wcslen', 'wcsncasecmp', 'wcsncasecmp_l', 'wcsncat', + 'wcsncmp', 'wcsncpy', 'wcsnlen', 'wcsnrtombs', 'wcspbrk', 'wcsrchr', 'wcsrtombs', 'wcsspn', 'wcsstr', 'wcstod', + 'wcstof', 'wcstoimax', 'wcstok', 'wcstol', 'wcstold', 'wcstold_l', 'wcstoll', 'wcstoll_l', 'wcstombs', 'wcstoul', + 'wcstoull', 'wcstoull_l', 'wcstoumax', 'wcswidth', 'wcsxfrm', 'wcsxfrm_l', 'wctob', 'wctomb', 'wctype', 'wctype_l', + 'wcwidth', 'wmemchr', 'wmemcmp', 'wmemcpy', 'wmemmove', 'wmempcpy', 'wmemset', 'wprintf', 'write', 'writev', + 'wscanf', 'main' +) + +# https://github.com/kaltura/kaltura-device-info-android +DRM_PLAYER = { + 'name': 'Kaltura Device Info', + 'package': 'com.kaltura.kalturadeviceinfo', + 'url': 'https://github.com/kaltura/kaltura-device-info-android/releases/download/t3/kaltura-device-info-release.apk', + 'path': Path(__file__).parent.parent / 'docs' / 'server' / 'kaltura.apk' +} + +DRM_WEB = 'https://bitmovin.com/demos/drm' diff --git a/keydive/adb/remote.py b/keydive/adb/remote.py new file mode 100644 index 0000000..17982d8 --- /dev/null +++ b/keydive/adb/remote.py @@ -0,0 +1,371 @@ +import logging +import re +import shutil + +from subprocess import run +from typing import Optional, List, Tuple +from pathlib import Path + +import frida +import requests + +from frida.core import Device + +# Suppress urllib3 warnings +logging.getLogger('urllib3.connectionpool').setLevel(logging.ERROR) + + +def shell(prompt: List[str]) -> Tuple[bool, str]: + """ + Executes a shell command and returns its success status along with the output. + + Args: + prompt (List[str]): A list representing the command and its arguments to be executed. + + Returns: + Tuple[bool, str]: A tuple where the first value is True if the command failed, + and the second value is the decoded standard output (stdout) string. + + Example: + shell(["ls", "-l"]) + (False, "total 0\n-rw-r--r-- 1 user ...") + + Note: + The return status is inverted (True means failure), which should be considered + when checking execution outcomes. + + """ + # Convert all arguments to strings in case any are not + prompt = list(map(str, prompt)) + + # Uncomment for debugging shell command execution + # logging.getLogger('Shell').debug('Executing command: %s', ' '.join(prompt)) + try: + # Execute the command and capture the output + # TODO: Redirect standard output and error in PIP + sp = run(prompt, capture_output=True) + + # Return True if the command failed (non-zero exit), along with stdout + return sp.returncode != 0, sp.stdout.decode('utf-8').strip() + except KeyboardInterrupt: + return False, '' + + +class Remote: + """ + Handles ADB-based operations on Android devices using Frida. + + This class provides a set of utilities for interacting with Android devices connected via ADB, + including retrieving system properties, managing applications, enumerating processes, and executing shell commands. + """ + + def __init__(self, serial: Optional[str] = None, timeout: int = 5): + """ + Initializes and connects to an Android device using ADB and Frida. + + Args: + serial (Optional[str]): Specific device serial number. If not provided, + the first connected USB device will be used. + timeout (int): Timeout (in seconds) for connecting to the device. + + Raises: + EnvironmentError: If ADB is not found in the system PATH. + Exception: If connection to the Android device fails. + KeyError: If essential properties (e.g., SDK or ABI) cannot be retrieved. + """ + self.logger = logging.getLogger('Remote') + + # Ensure ADB is installed and available in the system PATH + if not shutil.which('adb'): + raise EnvironmentError( + 'ADB is not recognized as an environment variable. ' + 'Ensure ADB is installed and refer to the documentation: ' + 'https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge' + ) + + # Start the ADB server if it's not already running + sp = shell(['adb', 'start-server']) + if sp[0]: + self.logger.warning('Unable to start ADB server (Error: %s)', sp[1]) + + # Attempt to connect to the device (or default to the first USB device) + try: + self.socket: Device = frida.get_device(serial, timeout) if serial else frida.get_usb_device(timeout) + self.logger.info('Connected to device: %s (%s)', self.socket.name, self.socket.id) + except Exception as e: + self.logger.critical('Could not connect to any device: %s', e) + raise e + + # Construct the shell command prefix using the connected device ID + self.__prefix = ['adb', '-s', self.socket.id, 'shell'] + + # Retrieve system properties (e.g., SDK version, CPU ABI) + properties = self.enumerate_properties() + + try: + self.sdk = properties['ro.build.version.sdk'] + self.abi = properties['ro.product.cpu.abi'] + except Exception as e: + raise KeyError('Failed to enumerate properties: %s', e) + + self.logger.info('SDK API: %s', self.sdk) + self.logger.info('ABI CPU: %s', self.abi) + + def enumerate_properties(self) -> dict: + """ + Retrieves Android system properties from the connected device via ADB. + + This method parses the output of the `getprop` shell command, casting values + to appropriate Python types (e.g., integers or booleans) when possible. It is + useful for gathering device configuration, build info, and environment metadata. + + Returns: + dict: A dictionary of system properties where keys are property names and + values are their corresponding parsed values. + """ + # https://source.android.com/docs/core/architecture/configuration/add-system-properties?#shell-commands + properties = {} + + # Execute shell command to fetch all system properties using 'getprop' + sp = shell([*self.__prefix, 'getprop', '|', 'strings']) + if sp[0]: + self.logger.error('Unable to retrieve system properties from the device (error: %s)', sp[1]) + return properties + + # Parse each line of the output and extract key-value pairs + for line in sp[1].splitlines(): + match = re.match(r'\[(.*?)]: \[(.*?)]', line) + if match: + key, value = match.groups() + + # Try casting numeric strings to integers + if value.isdigit(): + value = int(value) + + # Try converting string booleans to Python boolean + elif value.lower() in ('true', 'false'): + value = value.lower() == 'true' + + # Store the parsed key-value pair + properties[key] = value + + return properties + + def enumerate_processes(self, pids: List[int] = None, names: List[str] = None) -> dict: + """ + Retrieves a list of running processes from the connected Android device. + + This method uses ADB to run the `ps` command, processes the output, and filters + the result by provided process IDs or names if specified. It gracefully handles + variability in output format across Android versions. + + Args: + pids (List[int], optional): A list of process IDs to filter against. Only + processes with these PIDs will be included in the result. + names (List[str], optional): A list of process names to filter against. + Only processes with these names will be included in the result. + + Returns: + dict: A dictionary mapping process IDs (int) to process names (str). + + Exception: + Logs and returns an empty dictionary if the `ps` command fails to execute. + """ + # https://github.com/frida/frida/issues/1225#issuecomment-604181822 + processes = {} + + # Attempt to get the list of processes using the 'ps -A' command + prompt = [*self.__prefix, 'ps'] + sp = shell([*prompt, '-A']) + lines = sp[1].splitlines() + + # Retry with simpler 'ps' if output seems too short (older Android or restricted shell) + if len(lines) < 10: + sp = shell(prompt) + if sp[0]: + self.logger.error('Failed to execute ps command (Error: %s)', sp[1]) + return processes + lines = sp[1].splitlines() + + # Iterate through process list, skipping the header + for line in lines[1:]: + try: + parts = line.split() # USER,PID,PPID,VSZ,RSS,WCHAN,ADDR,S,NAME + pid = int(parts[1]) # Extract the PID from the 2nd column + name = ' '.join(parts[8:]).strip() # Extract the process name (column 9+) + + # Handle cases where process name might be in brackets (e.g., kernel threads) + name = name if name.startswith('[') else Path(name).name + + # Apply optional filters + if (pids and pid not in pids) or (names and name not in names): + continue + processes[pid] = name + except Exception as e: + # Suppress and skip lines that cannot be parsed correctly + pass + + return processes + + def enumerate_applications(self, user: bool = True, system: bool = False) -> dict: + """ + Lists installed Android applications on the connected device. + + This method uses ADB's package manager (pm) to list installed apps, + with optional filtering for user-installed or system apps. It returns + a mapping of package names to their corresponding APK file paths. + + Args: + user (bool): If True, include user-installed applications (default: True). + system (bool): If True, include system applications (default: False). + + Returns: + dict: A dictionary where the keys are package names (str) and the values + are file paths (str) to their APKs. + """ + applications = {} + + # Validate input to ensure at least one filter is active + if not user and not system: + return applications # Nothing to return if both are disabled + + # Construct the command to retrieve package info + prompt = [*self.__prefix, 'pm', 'list', 'packages', '-f'] + if user and not system: + prompt.append('-3') # Filter only user-installed apps + elif not user and system: + prompt.append('-s') # Filter only system apps + + # Run the shell command to get the list of packages + sp = shell(prompt) + if sp[0]: + self.logger.error('Unable to list installed apps (Error: %s)', sp[0]) + return applications + + # Parse command output line-by-line + for line in sp[1].splitlines(): + try: + # Example line format: package:/data/app/com.example.app-1/base.apk=com.example.app + path, package = line.strip().split(':', 1)[1].rsplit('=', 1) + applications[package] = path + except Exception as e: + # Skip lines that do not conform to expected structure + pass + + return applications + + def open_url(self, url: str) -> bool: + """ + Launches the specified URL on the connected Android device using the default browser. + + This uses Android's Activity Manager (am) to start an intent with the action + `android.intent.action.VIEW`, which is the standard way to launch URLs. + + Args: + url (str): The web address to open on the device (e.g., "https://example.com"). + + Returns: + bool: True if the command executed successfully and the intent was launched; + False if the shell command failed or was rejected by the system. + """ + # Execute the shell command to open the URL using the Android 'am' (Activity Manager) command. + sp = shell([*self.__prefix, 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', url]) + if sp[0]: + self.logger.error('Failed to open URL: %s (Error: %s)', url, sp[1]) + + # Return True if command succeeded (sp[0] is 0), False otherwise + return not sp[0] + + def install_application(self, path: Optional[Path] = None, url: Optional[str] = None) -> bool: + """ + Installs an APK on the connected Android device from either a local file or a remote URL. + + Args: + path (Optional[Path]): Path to the APK file stored locally. + url (Optional[str]): Direct URL to download the APK from. + + Returns: + bool: True if the application was successfully installed, False otherwise. + """ + # Prepare the shell command for installation + prompt = [*self.__prefix[:-1], 'install'] + + # If a valid local path is provided, attempt to install the APK from that file + if path and path.is_file(): + sp = shell([*prompt, path]) # Run the installation command with the local file path + if not sp[0]: + return True # Installation succeeded + self.logger.error('Could not install the APK from local path: %s (Error: %s)', path, sp[1]) + + # If a URL is provided, try to download the APK and install it temporarily + status = False + if url: + file = Path('tmp.apk') # Temporary file to store the downloaded APK + try: + # Send a GET request to download the APK + r = requests.request( + method='GET', + url=url, + headers={ + 'Accept': '*/*', + 'User-Agent': 'KeyDive/ADB' + } + ) + r.raise_for_status() + + # Write the downloaded content to a temporary APK file + file.write_bytes(r.content) + + # Attempt to install the downloaded APK + status = self.install_application(path=file) + except Exception as e: + self.logger.error('Failed to download or install APK from URL: %s (Error: %s)', url, e) + finally: + # Clean up the temporary file regardless of success or failure + file.unlink(missing_ok=True) + + return status + + def start_application(self, package: str) -> bool: + """ + Attempts to start an Android application using its package name by locating + and invoking its main activity via the Activity Manager. + + Args: + package (str): The package name of the application to start. + + Returns: + bool: True if the application was successfully launched, False otherwise. + """ + # Get package information using dumpsys + sp = shell([*self.__prefix, 'dumpsys', 'package', package]) + if sp[0]: + self.logger.error('Unable to retrieve package information (Error: %s)', sp[1]) + return False + + # Clean up output and filter out empty lines for consistency + lines = sp[1].splitlines() + lines = [l.strip() for l in lines if l.strip()] + + # Scan for the MAIN intent line and extract the associated activity + for i, line in enumerate(lines): + if 'android.intent.action.MAIN' in line: + match = re.search(fr'({package}/[^ ]+)', lines[i + 1]) + if match: + # Format: com.example.package/.MainActivity + main_activity = match.group() + + # Attempt to start the application using the resolved activity + sp = shell([*self.__prefix, 'am', 'start', '-n', main_activity]) + if not sp[0]: + return True + + self.logger.error('Failed to start app %s (Error: %s)', package, sp[1]) + break + + # TODO: adb shell monkey -p com.topjohnwu.magisk -c android.intent.category.LAUNCHER 1 + self.logger.error('No MAIN activity found for package "%s" or package not installed.', package) + return False + + +__all__ = ('Remote',) diff --git a/keydive/adb/vendor.py b/keydive/adb/vendor.py new file mode 100644 index 0000000..8d9ea27 --- /dev/null +++ b/keydive/adb/vendor.py @@ -0,0 +1,114 @@ +import re + +from typing import List, Optional, Tuple + + +class Vendor: + """ + Represents a vendor-specific configuration including process name, minimum SDK version, + OEM versioning information, and the name of a related native library. + + This class is used to identify vendor-specific behaviors on Android devices and provides + regex-based helpers for pattern matching against processes and libraries. + """ + + def __init__( + self, + process: str, + min_sdk: int, + min_oem: Tuple[int, str], + library: str + ): + """ + Initializes a Vendor instance with identifying details. + + Args: + process (str): The name of the process to identify for the vendor (can be pattern-based). + min_sdk (int): The minimum required Android SDK version for compatibility. + min_oem (Tuple[int, str]): Tuple defining the minimum OEM version, e.g., (version_code, version_name). + library (str): The shared library name or pattern used by the vendor. + + """ + self.process = process + self.min_sdk = min_sdk + self.min_oem = min_oem + self.library = library + + def __repr__(self) -> str: + """ + Generates a string representation of the current instance for debugging purposes. + + This method is primarily used to help developers understand the state of the object + during logging or interactive debugging by displaying all attribute names and values + in a clean, readable format. + + Returns: + str: A string showing the class name and its instance variables with their values. + """ + # Construct a formatted string with the class name and a list of key=value pairs from the instance's attributes + return '{name}({items})'.format( + name=self.__class__.__name__, + items=', '.join([f'{k}={repr(v)}' for k, v in self.__dict__.items()]) + ) + + @staticmethod + def __pattern(value: str) -> str: + """ + Converts a given string into a regex-compatible pattern that allows for matching + flexible naming conventions found in Android service or library identifiers. + + This is particularly useful when dealing with variant suffixes (e.g., `-lazy`, version tags), + or optional annotations (e.g., `@` in `.so` or `.widevine` modules). + + Args: + value (str): The base string (e.g., filename or service name) to convert into a regex pattern. + + Returns: + str: A regex pattern that can be used to match common variants of the input string. + """ + return (value + .replace('-service', r'-service(?:-lazy)?') + .replace('.widevine', r'.widevine(?:@\S+)?') + .replace('.so', r'(?:@\S+)?.so') + .replace('.', r'\.')) + + def is_process(self, value: str) -> bool: + """ + Determines whether the provided process name matches the expected vendor-specific process pattern. + + This is useful when trying to detect or hook into a known target process, + especially when its name can include runtime-specific suffixes (e.g., versioned .so files or services). + + Args: + value (str): The name of the process to evaluate. + + Returns: + bool: True if the process name matches the generated pattern; otherwise, False. + """ + # Generate a regex pattern from the expected process name + pattern = self.__pattern(self.process) + + # Check if the input value matches the regex pattern + return bool(re.match(pattern, value)) + + def get_library(self, values: List[dict]) -> Optional[dict]: + """ + Retrieves the first matching library dictionary from a provided list based on a dynamically generated regex pattern. + + This method is useful when trying to identify a target library in a list of shared objects (e.g., `.so` files), + especially when the exact name may include version tags or runtime decorations (e.g., `libxyz@1.2.so`). + + Args: + values (List[dict]): A list of dictionaries, each expected to contain a 'name' key with the library's filename. + + Returns: + Optional[dict]: The first dictionary whose 'name' matches the expected regex pattern; returns None if no match is found. + """ + # Create a regex pattern based on the expected library name, allowing flexibility for suffixes or variants + pattern = self.__pattern(self.library) + + # Iterate through the list and return the first dictionary with a matching 'name' + return next((v for v in values if re.match(pattern, v['name'])), None) + + +__all__ = ('Vendor',) diff --git a/keydive/cdm.py b/keydive/cdm.py deleted file mode 100644 index 72413d0..0000000 --- a/keydive/cdm.py +++ /dev/null @@ -1,279 +0,0 @@ -import base64 -import json -import logging - -from typing import Union -from zlib import crc32 -from unidecode import unidecode -from pathlib import Path - -from pathvalidate import sanitize_filepath, sanitize_filename -from Cryptodome.PublicKey import RSA -from Cryptodome.PublicKey.RSA import RsaKey -from pywidevine.device import Device, DeviceTypes -from pywidevine.license_protocol_pb2 import ( - SignedMessage, LicenseRequest, ClientIdentification, SignedDrmCertificate, DrmCertificate, - EncryptedClientIdentification) - -from keydive.constants import OEM_CRYPTO_API -from keydive.keybox import Keybox - - -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, keybox: bool = False): - """ - Initializes the Cdm object, setting up a logger and containers for client IDs and private keys. - - Parameters: - keybox (bool, optional): Initializes a Keybox instance for secure key management. - """ - self.logger = logging.getLogger(self.__class__.__name__) - # https://github.com/devine-dl/pywidevine - self.client_id: dict[int, ClientIdentification] = {} - self.private_key: dict[int, RsaKey] = {} - - # Optionally initialize a Keybox instance for secure key management if 'keybox' is True - self.keybox = Keybox() if keybox else None - - @staticmethod - def __client_info(client_id: ClientIdentification) -> dict: - """ - Converts client identification information to a dictionary. - - Parameters: - 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} - - @staticmethod - def __encrypted_client_info(encrypted_client_id: EncryptedClientIdentification) -> dict: - """ - Converts encrypted client identification information to a dictionary. - - Parameters: - encrypted_client_id (EncryptedClientIdentification): The encrypted client identification. - - Returns: - dict: A dictionary of encrypted client information. - """ - content = { - "providerId": encrypted_client_id.provider_id, - "serviceCertificateSerialNumber": encrypted_client_id.service_certificate_serial_number, - "encryptedClientId": encrypted_client_id.encrypted_client_id, - "encryptedClientIdIv": encrypted_client_id.encrypted_client_id_iv, - "encryptedPrivacyKey": encrypted_client_id.encrypted_privacy_key - } - return { - k: base64.b64encode(v).decode("utf-8") if isinstance(v, bytes) else v - for k, v in content.items() - } - - def set_challenge(self, data: Union[Path, bytes]) -> None: - """ - Sets the challenge data by extracting device information and client ID. - - Parameters: - data (Union[Path, bytes]): Challenge data as a file path or raw bytes. - - Raises: - FileNotFoundError: If the file path doesn't exist. - Exception: Logs any other exceptions that occur. - """ - try: - # Check if the data is a Path object, indicating it's a file path - if isinstance(data, Path): - data = data.read_bytes() - - # Parse the signed message from the data - signed_message = SignedMessage() - signed_message.ParseFromString(data) - - # Parse the license request from the signed message - license_request = LicenseRequest() - license_request.ParseFromString(signed_message.msg) - - # Extract the encrypted client ID, if available - # https://integration.widevine.com/diagnostics - encrypted_client_id: EncryptedClientIdentification = license_request.encrypted_client_id - if encrypted_client_id.SerializeToString(): - # If encrypted, log the encrypted client ID and indicate encryption - self.logger.info("Receive encrypted client id: \n\n%s\n", json.dumps(self.__encrypted_client_info(encrypted_client_id), indent=2)) - self.logger.warning("The client ID of the challenge is encrypted") - else: - # If unencrypted, extract and set the client ID - client_id: ClientIdentification = license_request.client_id - self.set_client_id(data=client_id) - - except FileNotFoundError as e: - raise FileNotFoundError(f"Challenge file not found: {data}") from e - except Exception as e: - self.logger.debug("Failed to set challenge data: %s", e) - - def set_private_key(self, data: Union[Path, bytes], name: str = None) -> None: - """ - Sets the private key from the provided data. - - Parameters: - data (Union[Path, bytes]): The private key data, either as a file path or byte data. - name (str, optional): Function name for verification against known functions. - - Raises: - FileNotFoundError: If the file path doesn't exist. - Exception: Logs any other exceptions that occur. - """ - try: - # Check if the data is a Path object, indicating it's a file path - if isinstance(data, Path): - data = data.read_bytes() - - # Import the private key using the RSA module - key = RSA.import_key(data) - - # Log the private key if it's not already in the dictionary - if key.n not in self.private_key: - self.logger.info("Receive private key: \n\n%s\n", key.exportKey("PEM").decode("utf-8")) - - # If a function name is provided, verify it against known functions - if name and name not in OEM_CRYPTO_API: - self.logger.warning("The function '%s' does not belong to the referenced functions. Communicate it to the developer to improve the tool.",name) - - # Store the private key in the dictionary, using the modulus (key.n) as the key - self.private_key[key.n] = key - except FileNotFoundError as e: - raise FileNotFoundError(f"Private key file not found: {data}") from e - except Exception as e: - self.logger.debug("Failed to set private key: %s", e) - - def set_client_id(self, data: Union[ClientIdentification, bytes]) -> None: - """ - Sets the client ID from the provided data. - - Parameters: - data (Union[ClientIdentification, bytes]): The client ID data. - """ - try: - # Check if the provided data is already a `ClientIdentification` object - if isinstance(data, ClientIdentification): - client_id = data - else: - # Deserialize the byte data into a `ClientIdentification` object - client_id = ClientIdentification() - client_id.ParseFromString(data) - - # Initialize objects for parsing the DRM certificate and signed certificate - signed_drm_certificate = SignedDrmCertificate() - drm_certificate = DrmCertificate() - - # Parse the signed DRM certificate from the client ID token - signed_drm_certificate.ParseFromString(client_id.token) - drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate) - - # Extract the public key from the DRM certificate - public_key = drm_certificate.public_key - key = RSA.importKey(public_key) - - # Check if this public key has already been recorded and log the client ID info - if key.n not in self.client_id: - self.logger.info("Receive client id: \n\n%s\n", json.dumps(self.__client_info(client_id), indent=2)) - - # Store the client ID in the client_id dictionary, using the public key modulus (`key.n`) as the key - self.client_id[key.n] = client_id - except Exception as e: - self.logger.debug("Failed to set client ID: %s", e) - - def set_device_id(self, data: bytes) -> None: - """ - Sets the device ID in the keybox. - - Parameters: - data (bytes): The device ID to be stored in the keybox. - """ - if self.keybox: - self.keybox.set_device_id(data=data) - - def set_keybox(self, data: bytes) -> None: - """ - Sets the keybox data. - - Parameters: - data (bytes): The keybox data to be set. - """ - if self.keybox: - self.keybox.set_keybox(data=data) - - def export(self, parent: Path, wvd: bool = False) -> bool: - """ - Exports client ID, private key, and optionally WVD files to disk. - - Parameters: - parent (Path): Directory to export the files to. - wvd (bool, optional): Whether to export WVD files. Defaults to False. - - Returns: - bool: True if any keys were exported, otherwise False. - """ - # Find the intersection of client IDs and private keys - keys = self.client_id.keys() & self.private_key.keys() - - for k in keys: - # Retrieve client information based on the client ID - 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 - ) - - # Generate a sanitized file path for exporting the data - # https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146958022 - parent = sanitize_filepath(parent / client_info["company_name"] / client_info["model_name"] / str(device.system_id) / str(k)[:10]) - parent.mkdir(parents=True, exist_ok=True) - - # Export the client ID to a binary file - 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) - - # Export the private key to a PEM file - 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 the WVD option is enabled, export the WVD file - if wvd: - # Serialize the device to WVD format - wvd_bin = device.dumps() - - # Generate a unique name for the WVD file using client and device details - 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 / sanitize_filename(f"{name}_{device.system_id}_l{device.security_level}.wvd") - - # Export the WVD file to disk - path_wvd.write_bytes(data=wvd_bin) - self.logger.info("Exported WVD: %s", path_wvd) - - # If keybox is available and hasn't been exported, issue a warning - if self.keybox and not self.keybox.export(parent=parent.parent): - self.logger.warning("The keybox has not been intercepted or decrypted") - - # Return True if any keys were exported, otherwise return False - return len(keys) > 0 - - -__all__ = ("Cdm",) diff --git a/keydive/constants.py b/keydive/constants.py deleted file mode 100644 index f64d2d3..0000000 --- a/keydive/constants.py +++ /dev/null @@ -1,145 +0,0 @@ -from pathlib import Path - -from keydive.vendor import Vendor - -# https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:bionic/libc/libc.map.txt -NATIVE_C_API = { - # BUILT-IN - "main", - # 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", "snprintf", - # 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" -} - -# https://cs.android.com/search?q=oemcrypto&sq=&ss=android%2Fplatform%2Fsuperproject -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", "ehdqmfmd", - "xftzvkwx", "gndskkuk", "wcggmnnx", "kaatohcz", "ktmgdchz", "jkcwonus", "ehmduqyt", "vewtuecx", "mxrbzntq", - "isyowgmp", "flzfkhbc", "rtgejgqb", "sxxprljw", "ebxjbtxl", "pcmtpkrj", "ncmqbmbc" - # Add more as needed for different versions. -} - -# https://developer.android.com/tools/releases/platforms -CDM_VENDOR_API = { - "mediaserver": [ - Vendor(22, 11, "1.0", r"libwvdrmengine(?:@\S+)?\.so") - ], - "mediadrmserver": [ - Vendor(24, 11, "1.0", r"libwvdrmengine(?:@\S+)?\.so") - ], - "android.hardware.drm@1.0-service.widevine": [ - Vendor(26, 13, "5.1.0", r"libwvhidl(?:@\S+)?\.so") - ], - "android.hardware.drm@1.1-service.widevine": [ - Vendor(28, 14, "14.0.0", r"libwvhidl(?:@\S+)?\.so") - ], - "android.hardware.drm@1.2-service.widevine": [ - Vendor(29, 15, "15.0.0", r"libwvhidl(?:@\S+)?\.so") - ], - "android.hardware.drm@1.3-service.widevine": [ - Vendor(30, 16, "16.0.0", r"libwvhidl(?:@\S+)?\.so") - ], - "android.hardware.drm@1.4-service.widevine": [ - Vendor(31, 16, "16.1.0", r"libwvhidl(?:@\S+)?\.so") - ], - "android.hardware.drm-service.widevine": [ - Vendor(33, 17, "17.0.0", r"libwvaidl(?:@\S+)?\.so"), - Vendor(34, 18, "18.0.0", r"android\.hardware\.drm-service(?:-lazy)?\.widevine(?:@\S+)?"), - Vendor(35, 18, "19.0.1", r"android\.hardware\.drm-service(?:-lazy)?\.widevine(?:@\S+)?") - ] -} - -# https://developers.google.com/widevine -CDM_FUNCTION_API = { - "UsePrivacyMode", - "GetCdmClientPropertySet", - "PrepareKeyRequest", - "getOemcryptoDeviceId", - "lcc07", - "oecc07", - "Read", - "x1c36", - "runningcrc" -} - -# Maximum clear API level for Keybox -# https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:trusty/user/app/sample/hwcrypto/keybox/keybox.c -KEYBOX_MAX_CLEAR_API = 28 - -# https://github.com/kaltura/kaltura-device-info-android -DRM_PLAYER = { - "package": "com.kaltura.kalturadeviceinfo", - "path": Path(__file__).parent.parent / "docs" / "server" / "kaltura.apk", - "url": "https://github.com/kaltura/kaltura-device-info-android/releases/download/t3/kaltura-device-info-release.apk" -} diff --git a/keydive/core.py b/keydive/core.py index ca4a005..384743c 100644 --- a/keydive/core.py +++ b/keydive/core.py @@ -1,228 +1,466 @@ -import json import logging -import re +import time +from typing import Optional, Tuple, Literal from pathlib import Path -import frida -import xmltodict - +from frida import ServerNotRunningError from frida.core import Session, Script +from pathvalidate import sanitize_filepath -from keydive.adb import ADB -from keydive.cdm import Cdm -from keydive.constants import OEM_CRYPTO_API, NATIVE_C_API, CDM_FUNCTION_API -from keydive.vendor import Vendor +from keydive.adb import DRM_PLAYER, DRM_WEB, NATIVE_C_API +from keydive.adb.remote import Remote +from keydive.adb.vendor import Vendor +from keydive.drm import CDM_VENDOR_API, OEM_CRYPTO_API +from keydive.drm.cdm import Cdm +from keydive.utils import xmldec, dumps -class Core: - """ - Core class for managing DRM operations and interactions with Android devices. - """ +class Server: - def __init__(self, adb: ADB, cdm: Cdm, functions: Path = None, skip: bool = False): + def __init__(self, version: str): """ - Initializes a Core instance. + Initialize the Server instance with the provided Frida version string. - Parameters: - adb (ADB): ADB instance for device communication. - cdm (Cdm): Instance for handling DRM-related operations. - functions (Path, optional): Path to Ghidra XML file for symbol extraction. Defaults to None. - skip (bool, optional): Whether to skip predefined functions (e.g., OEM_CRYPTO_API). Defaults to False. + This constructor sets internal flags based on the Frida version, + particularly to determine support for features introduced in Frida 16.6.0+. + + Args: + version (str): The version string of the Frida server (e.g., "16.5.3"). """ - self.logger = logging.getLogger(self.__class__.__name__) - self.running = True - self.cdm = cdm - self.adb = adb + self.logger = logging.getLogger('Server') - # Flag to skip predefined functions based on the vendor's API level - # https://github.com/hyugogirubato/KeyDive/issues/38#issuecomment-2411932679 - self.skip = skip + self.version = version + self.logger.debug('Frida version: %s', self.version) - # Load the hook script with relevant data and prepare for injection - self.functions = functions - self.script = self.__prepare_hook_script() - self.logger.info("Hook script prepared successfully") + # Parse version string and determine feature support + code = tuple(map(int, version.split('.'))) + self.features = code[0] > 16 or (code[0] == 16 and code[1] >= 6) + self.logger.debug('Feature support: %s', self.features) - def __prepare_hook_script(self) -> str: + # Used to prevent repeated user warnings in logs or dialogs + self.dialog = False + + +class Core(Remote): + + def __init__( + self, + serial: Optional[str] = None, + timeout: int = 5, + symbols: Optional[Path] = None, + detect: bool = True, + disabler: bool = True, + unencrypt: bool = False + ): """ - Prepares the hook script by injecting library-specific data. + Initialize the Core object, which manages interaction with a remote device + and handles Widevine CDM (Content Decryption Module) processes. + + This constructor also prepares a Frida script for hooking and sets up + internal state for later process attachment and monitoring. + + Args: + serial (Optional[str]): The serial number of the connected Android device. + If None, the first available device will be used. + timeout (int): Timeout in seconds for establishing the device connection. + symbols (Optional[Path]): Path to an XML file containing symbol definitions. + detect (bool): Whether to enable detection logic in the injected Frida script. + disabler (bool): Whether to enable anti-tamper or anti-debugging disablers + in the Frida script. + unencrypt (bool): If True, forces license challenge data to be sent unencrypted. + """ + # Initialize base Remote object with optional serial and connection timeout + super().__init__(serial=serial, timeout=timeout) + self.logger = logging.getLogger('Core') + + # Indicates whether the watchdog process is actively running + self._running = False + + # Server instance (initialized later when Frida is attached) + self._server: Optional[Server] = None + + # Load and prepare the Frida script with optional symbol resolution + self._script, self._resolved = self.__hook_script( + detect=detect, + disabler=disabler, + path=symbols, + unencrypt=unencrypt + ) + + # Initialize CDM (Content Decryption Module) interface with SDK and disabler flag + self.cdm = Cdm(sdk=self.sdk, disabler=disabler) + + def __hook_script( + self, detect: bool = True, + disabler: bool = True, + path: Optional[Path] = None, + unencrypt: bool = False + ) -> Tuple[str, bool]: + """ + Prepares and customizes a JavaScript hook script with runtime configurations and optional symbol resolution. + + This function reads a base JavaScript file (`keydive.js`), injects configurations and API stubs into it, + and optionally replaces placeholders with resolved symbol names extracted from an XML file describing + native functions. + + Args: + detect (bool): Whether to enable the detection logic in the hook script. + disabler (bool): Whether to enable the disabler logic in the hook script. + path (Optional[Path]): An optional path to an XML file containing function metadata (e.g., Ghidra-exported data). + If provided, function symbols are parsed and injected into the script. + unencrypt (bool): If True, configures the hook script to force client ID and related license + challenge data to remain unencrypted. Returns: - str: The finalized hook script content with placeholders replaced. + Tuple[str, bool]: A tuple where: + - str is the fully prepared hook script as a string. + - bool indicates whether symbol data was successfully loaded and injected. + + Exception: + Exceptions raised while reading or parsing the symbol file are caught and logged. """ - # Read the base JavaScript template file - content = Path(__file__).with_name("keydive.js").read_text(encoding="utf-8") + # Load the base hook JavaScript file + script = Path(__file__).with_name('keydive.js').read_text(encoding='utf-8') - # Generate the list of symbols from the functions file - symbols = self.__prepare_symbols(self.functions) + symbols = {} + if path: + try: + # Parse the XML file containing function metadata (e.g., Ghidra export) + content = xmldec(path.read_bytes(), force_list=['FUNCTION', 'STACK_VAR', 'REGISTER_VAR']) + program = content['PROGRAM'] + addr_base = int(program['@IMAGE_BASE'], 16) + functions = program['FUNCTIONS']['FUNCTION'] - # Define the placeholder replacements - replacements = { - "${OEM_CRYPTO_API}": json.dumps(list(OEM_CRYPTO_API)), - "${NATIVE_C_API}": json.dumps(list(NATIVE_C_API)), - "${SYMBOLS}": json.dumps(symbols), - "${SKIP}": str(self.skip) + # Build a dictionary of function address to name mappings + for f in functions: + address = hex(int(f['@ENTRY_POINT'], 16) - addr_base) + name = f['@NAME'] + + # Avoid duplicate entries by checking existing symbol names + if name not in symbols.values(): + symbols[address] = name + + self.logger.info('Successfully loaded %d symbol(s) from: %s', len(symbols), path.as_posix()) + except Exception as e: + # Log the error if symbol import fails, but continue script generation + self.logger.error('Unable to import symbols from XML: %s', e) + + # Create the placeholder-to-value mapping + placeholders = { + '${OEM_CRYPTO_API}': dumps(OEM_CRYPTO_API), + '${NATIVE_C_API}': dumps(NATIVE_C_API), + '${SYMBOLS}': dumps(symbols), + '${DETECT}': str(detect), + '${DISABLER}': str(disabler), + '${UNENCRYPT}': str(unencrypt) } - # Replace placeholders in the script content - for placeholder, value in replacements.items(): - content = content.replace(placeholder, value, 1) + # Replace placeholders in the script with actual data + for placeholder, value in placeholders.items(): + script = script.replace(placeholder, value, 1) - return content + # Return the modified script and a flag indicating if symbol injection was successful + return script, bool(symbols) - def __prepare_symbols(self, path: Path) -> list: + def launch(self, action: Literal['web', 'player'] = 'player') -> None: """ - Extracts relevant functions from a Ghidra XML file. + Launches a DRM-enabled player either as a native application or a web-based player. - Parameters: - path (Path): Path to the Ghidra XML functions file. + Depending on the selected action, this method ensures that the DRM player app is installed and running, + or opens a predefined DRM test page in the system browser. It performs basic checks for installation, + process existence, and handles optional installation if needed. - Returns: - list: List of selected functions as dictionaries. + Args: + action (Literal['web', 'player']): + - 'player': Launch the native DRM player app. + - 'web': Open the web-based DRM player in the default browser. + """ + if action == 'player': + # Retrieve the package name and human-readable name of the DRM player + player_package = DRM_PLAYER['package'] + player_name = DRM_PLAYER['name'] + + self.logger.info('Preparing DRM player: %s (%s)', player_name, player_package) + + # Check if the application is already installed + installed = player_package in self.enumerate_applications(user=True, system=False) + if installed: + self.logger.debug('Application is already installed: %s', player_package) + else: + self.logger.debug('Application not found: %s. Attempting to install...', player_package) + + # Try to install the app from the specified path or URL + if not self.install_application(path=DRM_PLAYER['path'], url=DRM_PLAYER['url']): + return # Stop if installation fails + self.logger.debug('Application installed successfully: %s', player_package) + + # Check if the application is already running + player_pid = self.enumerate_processes(names=[player_package]) + if player_pid: + self.logger.warning('Application is already running: %s (%s)', player_name, player_package) + else: + # Attempt to start the application + self.logger.info('Starting application: %s (%s)', player_name, player_package) + if not self.start_application(player_package): + return # Abort if unable to launch + + # Give the system a moment to register the new process + time.sleep(1) + player_pid = self.enumerate_processes(names=[player_package]) + + # Log running process info or fallback if PID is unknown + if player_pid: + self.logger.debug('Running process: %s (%s)', list(player_pid.keys())[-1], player_package) + else: + self.logger.debug('Unable to determine PID (%s).', player_package) + + elif action == 'web': + # Attempt to open the DRM test URL in the default browser + self.logger.info('Opening DRM web player in default browser...') + if not self.open_url(DRM_WEB): + return + + # Attempt to detect a running browser process + player_pid = self.enumerate_processes(names=[ + 'com.android.chrome', # Google Chrome + 'com.sec.android.app.sbrowser', # Samsung Internet + 'org.mozilla.firefox' # Mozilla Firefox + ]) + + if player_pid: + self.logger.debug('Running process: %s (%s)', list(player_pid.keys())[-1], list(player_pid.values())[-1]) + else: + self.logger.debug('No known browser process detected. Consider adding support for your browser.') + + def watchdog( + self, + output: Path, + delay: int = 1, + auto_stop: bool = True, + wvd: bool = False, + keybox: bool = False + ) -> None: + """ + Continuously monitor Widevine DRM processes, export device credentials, and save to disk. + + This method acts as a watchdog that periodically scans for Widevine-capable processes, + attaches hooks to them, and exports relevant DRM credential files to a specified output directory. + It supports optional exporting in .wvd format and OEM keyboxes. + + Args: + output (Path): Directory path where extracted files will be saved. + delay (int, optional): Delay in seconds between each monitoring iteration. Defaults to 1. + auto_stop (bool, optional): Automatically stop after successful export of client_id.bin. Defaults to True. + wvd (bool, optional): Enable exporting credentials in .wvd format compatible with pywidevine. Defaults to False. + keybox (bool, optional): Enable exporting OEM certificates and keyboxes if available. Defaults to False. Raises: - FileNotFoundError: If the functions file is not found. - ValueError: If functions extraction fails. + EnvironmentError: Raised if no Widevine DRM process is detected during scanning. """ - # Return an empty list if no path is provided - if not path: - return [] + self.logger.info('Watcher delay: %ss' % delay) + self._running = True - try: - # Parse the XML file and extract program data - program = xmltodict.parse(path.read_bytes())["PROGRAM"] - addr_base = int(program["@IMAGE_BASE"], 16) # Base address for function addresses - functions = program["FUNCTIONS"]["FUNCTION"] # List of functions in the XML + extracted = [] # Tracks already exported files to avoid duplicates + current = None # Holds the PID of the currently hooked Widevine process - # Identify a target function from the predefined OEM_CRYPTO_API list (if not skipped) - target = next((f["@NAME"] for f in functions if f["@NAME"] in OEM_CRYPTO_API and not self.skip), None) + while self._running: + # Export device credentials and DRM data files + files = self.cdm.export(wvd=wvd, keybox=keybox) + if files: + for name, data in files.items(): + # Sanitize file path to ensure valid filesystem names and create directories + # https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146958022 + path = output / sanitize_filepath(name) + path.parent.mkdir(parents=True, exist_ok=True) - # Prepare a dictionary to store selected functions - selected = {} - for func in functions: - name = func["@NAME"] # Function name - args = len(func.get("REGISTER_VAR", [])) # Number of arguments + # If the file already exists and is identical, log a warning (only once) and skip + if path.is_file() and path.read_bytes() == data: + if path not in extracted: + self.logger.warning('File already exists: %s', path) + extracted.append(path) + continue - """ - Add the function if it matches specific criteria - - Match the target function if identified - - Match API keywords - - Match unnamed functions with 6+ args - """ - if name not in selected and ( - name == target - or any(True if self.skip else 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) # Calculate relative address - } + # If exporting a .wvd file, remove any other .wvd files in the same directory + if path.suffix == '.wvd' and path.parent.exists(): + for file in path.parent.glob('*.wvd'): + file.unlink() - # Return the list of selected functions - return list(selected.values()) - except FileNotFoundError as e: - raise FileNotFoundError(f"Functions file not found: {path}") from e - except Exception as e: - raise ValueError("Failed to extract functions from Ghidra XML file") from e + # Write the file + self.logger.info('Exporting file: %s', path) + path.write_bytes(data) + extracted.append(path) + + # If the client ID has been exported and auto-stop is enabled, stop monitoring + if 'client_id.bin' in dumps(extracted): + self._running = not auto_stop + + if not self._running: + self.logger.info('Required files exported successfully.') + continue + + # Detect Widevine-capable processes running on the device + processes = sorted( + [ + (pid, (name, vendor)) + for pid, name in self.enumerate_processes().items() + for vendor in CDM_VENDOR_API + if vendor.min_sdk <= self.sdk and vendor.is_process(name) + ], + key=lambda item: item[1][1].min_sdk, + reverse=True + ) + + if not processes: + # No DRM process detected, raise error with guidance for the user + raise EnvironmentError( + 'No Widevine DRM process detected. ' + 'Refer to: https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#drm-info' + ) + + # If previously hooked process is no longer running, clear current hook + if current and current not in [pid for pid, _ in processes]: + self.logger.warning('Widevine process terminated or replaced') + current = None + + # If no current hooked process, attempt to hook one from detected processes + if not current: + self.logger.debug('Scanning for Widevine processes...') + for pid, (name, vendor) in processes: + self.logger.info('Detected process: %s (%s)', pid, name) + if self.__hook_process(pid, vendor): + current = pid + break + + if current: + self.logger.info('Successfully attached hook to process: %s', current) + else: + self.logger.warning('Widevine library not located yet. Retrying...') + + # Sleep before next monitoring iteration to reduce resource usage + time.sleep(delay) def __process_message(self, message: dict, data: bytes) -> None: """ - Handles messages received from the Frida script. + Callback handler for messages sent from the Frida-injected script. - Parameters: - message (dict): The message payload. - data (bytes): The raw data associated with the message. + This function processes various types of messages and data received from the Frida instrumentation layer. + It routes them to appropriate CDM (Content Decryption Module) handlers or logs them accordingly. + + Args: + message (dict): The message dictionary sent from the Frida script. + data (bytes): The associated binary payload, if any. """ - logger = logging.getLogger("Script") - level = message.get("payload") + logger = logging.getLogger('Script') + level = message.get('payload') if isinstance(level, int): - # Log the message based on its severity level - logger.log(level=level, msg=data.decode("utf-8")) + # Log text message from script with given severity level + logger.log(level=level, msg=data.decode('utf-8')) + + # Stop the runner if a critical or fatal error is reported if level in (logging.FATAL, logging.CRITICAL): - self.running = False # Stop the process on critical errors - elif isinstance(level, dict) and "private_key" in level: - # Set the private key in the DRM handler - self.cdm.set_private_key(data=data, name=level["private_key"]) - elif level == "challenge": - # Set the challenge data in the DRM handler - self.cdm.set_challenge(data=data) - elif level == "device_id": - # Set the device ID in the DRM handler - self.cdm.set_device_id(data) - elif level == "keybox": - # Set the keybox data in the DRM handler + self.running = False + + elif isinstance(level, dict) and 'private_key' in level: + self.cdm.set_private_key(data, level['private_key']) + elif level == 'challenge': + self.cdm.set_challenge(data) + elif level == 'client_id': + self.cdm.set_client_id(data) + elif level == 'keybox': self.cdm.set_keybox(data) + elif level == 'stable_id': + self.cdm.set_stable_id(data) + elif level == 'device_id': + self.cdm.set_device_id(data) + elif level == 'provisioning_method': + self.cdm.set_provisioning_method(data) + elif level == 'provisioning_response': + self.cdm.set_provisioning_response(data) + elif message.get('type') == 'error': + # Log any script-side errors + logger.error(message['description']) + else: + # Fallback logging for unrecognized messages + logger.warning(message, data) - def hook_process(self, pid: int, vendor: Vendor, timeout: int = 0) -> bool: + def __hook_process(self, pid: int, vendor: Vendor, timeout: int = 0) -> bool: """ - Hooks into the specified process. + Hooks into a running process using Frida to enable dynamic analysis and instrumentation. - Parameters: - 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. + This method attaches to a process identified by its PID, loads a Frida script for hooking into + a vendor-specific shared library, and optionally enables symbolic analysis depending on device + and server capabilities. + + Args: + pid (int): Process ID of the target application to attach to. + vendor (Vendor): A Vendor object containing metadata about the expected shared library. + timeout (int, optional): Time (in seconds) to persist the Frida session. Defaults to 0. Returns: - bool: True if the process was successfully hooked, otherwise False. + bool: True if the hook was successful, False otherwise. + + Raises: + EnvironmentError: If the Frida server is not running on the target device. """ try: - # Attach to the target process using the specified PID. - # The 'persist_timeout' parameter ensures the session persists for the given duration. - session: Session = self.adb.device.attach(pid, persist_timeout=timeout) - except frida.ServerNotRunningError as e: - # Handle the case where the Frida server is not running on the device. - raise EnvironmentError("Frida server is not running") from e + # Attach to the target process using the specified PID + # The 'persist_timeout' parameter ensures the session persists for the given duration + session: Session = self.socket.attach(pid, persist_timeout=timeout) + except ServerNotRunningError as e: + # Handle the case where the Frida server is not running on the device + raise EnvironmentError('Frida server is not running on the device.') from e except Exception as e: - # Log other exceptions and return False to indicate failure. - self.logger.error(e) + # Catch and log all other errors that occur during session attachment + self.logger.error('Could not attach to process %s: %s', pid, e) return False - # Define a callback to handle when the process is destroyed. + # Define cleanup behavior for when the Frida script is destroyed def __process_destroyed() -> None: session.detach() - # Create a Frida script object using the prepared script content. - script: Script = session.create_script(self.script) - script.on("message", self.__process_message) - script.on("destroyed", __process_destroyed) + # Create and load the Frida script for dynamic analysis + script: Script = session.create_script(self._script) + script.on('message', self.__process_message) # Set message handler + script.on('destroyed', __process_destroyed) # Set destruction handler script.load() - # Fetch a list of libraries loaded by the target process. + # Fetch Frida server metadata (e.g., version) and store it if not already set + if not self._server: + self._server = Server(script.exports_sync.getversion()) + + # Retrieve the list of loaded libraries in the target process libraries = script.exports_sync.getlibraries() - library = next((l for l in libraries if re.match(vendor.pattern, l["name"])), None) + library = vendor.get_library(libraries) if library: - # Log information about the library if it is found. - self.logger.info("Library: %s (%s)", library["name"], library["path"]) + # Log details about the matching library + self.logger.info('Library found: %s (%s)', library['name'], library['path']) - # Retrieve and log the version of the Frida server. - version = script.exports_sync.getversion() - self.logger.debug(f"Server: %s", version) + # Provide context-aware warnings about symbol resolution options + if not self._server.dialog: + if self._server.features and self._resolved: + self.logger.warning('The "--symbols" option is deprecated in Frida 16.6.0 and newer.') + elif not self._server.features and vendor.min_oem[0] < 18 and self._resolved: + self.logger.warning('The "--symbols" option is deprecated for OEM API versions below 18.') + elif not self._server.features and vendor.min_oem[0] > 17 and not self._resolved: + self.logger.warning( + 'For OEM API > 17, the "--symbols" option is required. ' + 'Refer to: https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md' + ) + self._server.dialog = True - # Determine if the Frida server version is older than 16.6.0. - code = tuple(map(int, version.split("."))) - minimum = code[0] > 16 or (code[0] == 16 and code[1] >= 6) + # Determine whether to enable dynamic symbol resolution based on server and vendor context + dynamic = self._server.features and vendor.min_oem[0] > 17 and not self._resolved - # Warn the user if certain conditions related to the functions option are met. - if minimum and self.functions: - self.logger.warning("The '--functions' option is deprecated starting from Frida 16.6.0") - elif not minimum and vendor.oem < 18 and self.functions: - self.logger.warning("The '--functions' option is deprecated for OEM API < 18") - elif not minimum and vendor.oem > 17 and not self.functions: - self.logger.warning("For OEM API > 17, specifying '--functions' is required. Refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md") + # Attempt to hook into the identified library with or without dynamic symbols + return script.exports_sync.hooklibrary(library['name'], dynamic) - # Enable dynamic analysis (symbols) only when necessary - dynamic = minimum and vendor.oem > 17 and not self.functions - return script.exports_sync.hooklibrary(library["name"], dynamic) - - # Unload the script if the target library is not found. + # If the expected library was not found, clean up and notify the user script.unload() - self.logger.warning("Library not found: %s" % vendor.pattern) + self.logger.warning('Expected library not found: %s' % vendor.library) return False -__all__ = ("Core",) +__all__ = ('Core',) diff --git a/keydive/drm/__init__.py b/keydive/drm/__init__.py new file mode 100644 index 0000000..50d0b71 --- /dev/null +++ b/keydive/drm/__init__.py @@ -0,0 +1,55 @@ +from keydive.adb.vendor import Vendor + +# Maximum clear API level for Keybox +# https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:trusty/user/app/sample/hwcrypto/keybox/keybox.c +KEYBOX_MAX_CLEAR_API = 28 + +# https://cs.android.com/search?q=oemcrypto&sq=&ss=android%2Fplatform%2Fsuperproject +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', 'ehdqmfmd', + 'xftzvkwx', 'gndskkuk', 'wcggmnnx', 'kaatohcz', 'ktmgdchz', 'jkcwonus', 'ehmduqyt', 'vewtuecx', 'mxrbzntq', + 'isyowgmp', 'flzfkhbc', 'rtgejgqb', 'sxxprljw', 'ebxjbtxl', 'pcmtpkrj', 'uegpdzus', 'ncmqbmbc' + # Add more as needed for different versions. +) + +# https://developers.google.com/widevine +CDM_FUNCTIONS = ( + 'UsePrivacyMode', + 'GetCdmClientPropertySet', + 'PrepareKeyRequest', + 'AesCbcKey', + 'Read', '_x1c36', + 'runningcrc', + '_oecc07', '_lcc07', + '_oecc04', '_lcc04', + '_oecc49', '_lcc49', + '_oecc12', '_lcc12', + '_oecc95', '_lcc95', + '_oecc21', '_lcc21', + 'GenerateDerivedKeys' +) + +# https://developer.android.com/tools/releases/platforms +CDM_VENDOR_API = ( + Vendor('mediaserver', 22, (11, '1.0'), 'libwvdrmengine.so'), + Vendor('mediadrmserver', 24, (11, '1.0'), 'libwvdrmengine.so'), + Vendor('android.hardware.drm@1.0-service.widevine', 26, (13, '5.1.0'), 'libwvhidl.so'), + Vendor('android.hardware.drm@1.1-service.widevine', 28, (14, '14.0.0'), 'libwvhidl.so'), + Vendor('android.hardware.drm@1.2-service.widevine', 29, (15, '15.0.0'), 'libwvhidl.so'), + Vendor('android.hardware.drm@1.3-service.widevine', 30, (16, '16.0.0'), 'libwvhidl.so'), + Vendor('android.hardware.drm@1.4-service.widevine', 31, (16, '16.1.0'), 'libwvhidl.so'), + Vendor('android.hardware.drm-service.widevine', 33, (17, '17.0.0'), 'libwvaidl.so'), + Vendor('android.hardware.drm-service.widevine', 34, (18, '18.0.0'), 'android.hardware.drm-service.widevine') +) + +# https://github.com/kaltura/kaltura-device-info-android/blob/master/app/src/main/java/com/kaltura/kalturadeviceinfo/Collector.java#L317 +CDM_PROPERTIES = { + 'CompanyName': 'ro.product.manufacturer', + 'ModelName': 'ro.product.model', + 'ArchitectureName': 'ro.product.cpu.abi', + 'DeviceName': 'ro.product.device', + 'ProductName': 'ro.product.name', + 'BuildInfo': 'ro.build.fingerprint' +} diff --git a/keydive/drm/cdm.py b/keydive/drm/cdm.py new file mode 100644 index 0000000..23e4e1c --- /dev/null +++ b/keydive/drm/cdm.py @@ -0,0 +1,1000 @@ +import json +import logging +import re + +from enum import Enum +from json.encoder import encode_basestring_ascii +from typing import Union, Dict, List, Optional +from pathlib import Path +from zlib import crc32 + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.padding import OAEP, MGF1 +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.ciphers import Cipher +from cryptography.hazmat.primitives.ciphers.algorithms import AES +from cryptography.hazmat.primitives.ciphers.modes import CBC +from cryptography.hazmat.primitives.hashes import SHA1 +from cryptography.hazmat.primitives.padding import PKCS7 +from cryptography.hazmat.primitives.serialization import pkcs7 +from cryptography.x509 import Certificate +from asn1crypto.core import Integer + +from keydive.drm import OEM_CRYPTO_API, KEYBOX_MAX_CLEAR_API +from keydive.drm.device import Device, DeviceTypes +from keydive.drm.keybox import KeyBox +from keydive.utils import dumps, b64enc, b64dec, unidec +from keydive.drm.protocol.license_pb2 import ( + LicenseRequest, SignedMessage, ProvisioningResponse, SignedProvisioningMessage, ClientIdentification, + EncryptedClientIdentification, DrmCertificate, SignedDrmCertificate +) + +kWidevineSystemIdExtensionOid = '1.3.6.1.4.1.11129.4.1.1' + + +class OEMCrypto_ProvisioningMethod(Enum): + """ + Enum representing different OEMCrypto provisioning methods. + """ + ProvisioningError = 0 # Device cannot be provisioned. + DrmCertificate = 1 # Device has baked-in DRM certificate (level 3 only). + Keybox = 2 # Device has factory-installed unique keybox. + OEMCertificate = 3 # Device has factory-installed OEM certificate. + + +def CryptoSession_ExtractSystemIdFromOemCert(cert: Certificate) -> int: + """ + Extracts the Widevine System ID from an OEM X.509 certificate. + + The System ID is expected to be embedded as a custom extension within the certificate, + identified by the `kWidevineSystemIdExtensionOid`. The extension contains an ASN.1-encoded + integer, which this function decodes and returns. + + Args: + cert (Certificate): An X.509 certificate expected to include the Widevine System ID + as a custom extension. + + Returns: + int: The decoded Widevine System ID. + + Raises: + ValueError: If the certificate does not contain the Widevine System ID extension. + """ + # Iterate through all extensions in the certificate to locate the custom Widevine System ID + for ext in cert.extensions: + # Match against the known OID for the Widevine System ID + if ext.oid.dotted_string == kWidevineSystemIdExtensionOid: + # The extension value contains a DER-encoded ASN.1 INTEGER + # Decode it using asn1crypto's Integer class and return its integer representation + return int(Integer.load(ext.value.value)) + + # If no matching extension was found, raise a meaningful error + raise ValueError('The certificate does not contain a Widevine System ID extension.') + + +def CryptoSession_GetSecurityLevel(cert: Certificate) -> Optional[int]: + """ + Determines the Widevine security level (L1, L2, or L3) from the subject field + of an X.509 certificate. + + The level is inferred by searching for substrings like "_L1_", "_L2_", or "_L3_" in + the subject string, which typically reflects the provisioning level of the device + (hardware-backed = L1, software-only = L3, etc). + + Args: + cert (Certificate): An X.509 certificate whose subject may contain the security level marker. + + Returns: + Optional[int]: The security level as an integer (1 = L1, 2 = L2, 3 = L3), + or None if no level marker is found in the subject. + """ + # Extract the certificate's subject string in a standardized format + subject = cert.subject.rfc4514_string() + + # kSecurityLevel (L1, L2, L3, Unknown) + # Check for known security level markers in the subject string + return next((level for level in range(1, 4) if f'_L{level}_' in subject), None) + + +class Cdm: + """ + Content Decryption Module (CDM) helper class for managing DRM provisioning + and cryptographic assets such as keys, certificates, and client identity + objects used in Widevine provisioning. + + This class supports initialization of internal caches and maps for managing + OEM-provided certificates, keyboxes, and session-specific cryptographic data. + """ + + def __init__(self, sdk: int, disabler: bool = True): + """ + Initializes the CDM instance with the specified SDK version and optional L1 disabler control. + + Sets up internal caches for keys, certificates, and client identity structures, + which are typically used during Widevine provisioning workflows. + + Args: + sdk (int): The Android SDK version (used to determine feature compatibility or restrictions). + disabler (bool, optional): If True, applies logic related to L1 disabling behavior. Defaults to True. + """ + self.logger = logging.getLogger('Cdm') + self._sdk = sdk + self._disabler = disabler + + self._device_aes_key: List[bytes] = [] + self._keybox: Dict[bytes, KeyBox] = {} # stable_id -> keybox + + self._client_id: Dict[int, ClientIdentification] = {} # public_key.n -> client_id + self._certificate: Dict[int, List[Certificate]] = {} # public_key.n -> oem_certificate + self._private_key: Dict[int, RSAPrivateKey] = {} # public_key.n -> private_key + + # Cached ClientIdentification instance representing the device’s current provisioning context + # This may be reused across multiple requests to avoid re-parsing + self._provisioning: Optional[ClientIdentification] = None + + @staticmethod + def __client_info(client_id: ClientIdentification) -> dict: + """ + Converts a ClientIdentification object into a dictionary containing its core information, + and optionally includes capability details if logging is set to DEBUG level. + + This is useful for structured logging or inspection of client identity during + the provisioning process. + + Args: + client_id (ClientIdentification): The client identification object that contains + basic client info and optional client capability fields. + + Returns: + dict: A dictionary of client info fields. If logging is set to DEBUG, + a 'capabilities' key is included with detailed capability values. + """ + # Extract base client_info fields and store them in a dictionary + infos = {e.name: e.value for e in client_id.client_info} + + # Check the global logger level; only include capabilities in DEBUG mode + level = logging.getLogger().getEffectiveLevel() + if level != logging.DEBUG: + return infos # Return only core info if not debugging + + capabilities = {} + # Iterate over all explicitly set fields in client_capabilities + for field, value in client_id.client_capabilities.ListFields(): + # Handle fields of enum type + if field.type == field.TYPE_ENUM: + if field.label == field.LABEL_REPEATED: + # Convert each enum value (integer) to its named representation + value = [field.enum_type.values_by_number[v].name for v in value] + else: + # Convert single enum value to its name + value = field.enum_type.values_by_number[value].name + + # Add the field and its (possibly transformed) value to the capabilities dictionary + capabilities[field.name] = value + + # Merge basic client info and capabilities into a single dictionary and return + return {**infos, 'capabilities': capabilities} + + @staticmethod + def __client_generic(client_id: ClientIdentification) -> ClientIdentification: + """ + Produces a simplified version of a ClientIdentification object by removing + select non-essential fields from the `client_info` list. This is useful + when generating a generic client profile that omits sensitive or dynamic data, + such as application-specific metadata. + + Args: + client_id (ClientIdentification): The original, full client identification + object containing metadata and capabilities. + + Returns: + ClientIdentification: A new instance with reduced client_info fields, preserving + core identity and capabilities but omitting values like app name, origin, and cert hash. + + Notes: + The fields being removed are typically: + - 'application_name': Identifies the calling app; often dynamic or sensitive. + - 'origin': Source domain or app origin; may vary per request or client. + - 'package_certificate_hash_bytes': May be tied to APK signing identity. + + This function ensures that the returned object retains its structure and utility + while discarding non-critical identifiers that might affect reproducibility or + comparability across sessions. + """ + # Define the fields that are considered dynamic or non-essential for generic use + excluded_fields = {'application_name', 'origin', 'package_certificate_hash_bytes'} + + # Create a filtered list excluding the above fields from client_info + filtered_client_info = [ + info for info in client_id.client_info + if info.name not in excluded_fields + ] + + # Return a new ClientIdentification object with the cleaned client_info list + return ClientIdentification( + type=client_id.type, + token=client_id.token, + client_info=filtered_client_info, + # provider_client_token=client_id.provider_client_token, + # license_counter=client_id.license_counter, + client_capabilities=client_id.client_capabilities, + # vmp_data=client_id.vmp_data, + # device_credentials=client_id.device_credentials + ) + + def set_challenge(self, data: Union[bytes, List[Path]]) -> None: + """ + Parses a Widevine license challenge to extract and register client identification data. + + This method accepts either raw protobuf bytes representing a SignedMessage or a list + of file paths containing such data. It supports recursive processing of multiple + challenge files and handles both encrypted and unencrypted client IDs. + + Args: + data (Union[bytes, List[Path]]): + - Raw protobuf data as bytes representing a SignedMessage, or + - A list of Path objects pointing to files containing SignedMessage data. + """ + # If input is a list of file paths, process each file individually + if isinstance(data, list): + for path in data: + try: + # Read file content as bytes and recursively process it + self.set_challenge(data=path.read_bytes()) + except Exception as e: + self.logger.error('Could not load challenge from file %s: %s', path, e) + + # Proceed only if data is raw bytes after potential recursive calls + if not isinstance(data, bytes): + return + + # https://integration.widevine.com/diagnostics + try: + # Parse the SignedMessage protobuf from raw data + signed_message = SignedMessage() + signed_message.ParseFromString(data) + + # Extract and parse the embedded LicenseRequest message + license_request = LicenseRequest() + license_request.ParseFromString(signed_message.msg) + + # Attempt to extract the encrypted client ID (if present) + encrypted_client_id: EncryptedClientIdentification = license_request.encrypted_client_id + if encrypted_client_id.SerializeToString(): + # Encrypted client ID found - log its details for debugging + """ + self.logger.info( + 'Received encrypted client ID: \n\n%s\n', + dumps({ + f.name: v for f, v in encrypted_client_id.ListFields() + }, beauty=True) + ) + """ + self.logger.warning('The client ID in this challenge is encrypted and cannot be processed directly.') + else: + # Unencrypted client ID is available - register it immediately + self.set_client_id(data=license_request.client_id) + except Exception as e: + # Log any parsing errors at debug level to avoid cluttering standard logs + self.logger.debug('Unable to register challenge data: %s', e) + + def set_client_id(self, data: Union[ClientIdentification, bytes]) -> None: + """ + Loads and registers a client ID from either a parsed ClientIdentification object + or raw protobuf bytes. Handles different client ID token types, such as DRM device + certificates, keyboxes, and OEM device certificates. + + Args: + data (Union[ClientIdentification, bytes]): + Either a ClientIdentification protobuf object or raw protobuf bytes representing it. + + Raises: + ValueError: If the client type is unsupported or certificate chain length is invalid. + """ + try: + # If already a parsed ClientIdentification object, use it directly + if isinstance(data, ClientIdentification): + client_id = data + else: + # Otherwise, parse the protobuf bytes into a ClientIdentification object + client_id = ClientIdentification() + client_id.ParseFromString(data) + + # Cache this client ID for provisioning context usage elsewhere + self._provisioning = client_id + + # Process based on the token type of the client ID + if client_id.type == ClientIdentification.TokenType.DRM_DEVICE_CERTIFICATE: + # Clean unnecessary fields to simplify the client representation + client_id = self.__client_generic(client_id) + + # Initialize protobuf objects to parse DRM certificates + signed_drm_certificate = SignedDrmCertificate() + drm_certificate = DrmCertificate() + + # Parse signed DRM certificate from client token + signed_drm_certificate.ParseFromString(client_id.token) + drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate) + + # Load the public key from the DRM certificate (DER format) + public_key = serialization.load_der_public_key( + data=drm_certificate.public_key, + backend=default_backend() + ) + + # Extract the RSA modulus as a unique client identifier + modulus = public_key.public_numbers().n + + # Register new or changed client IDs keyed by modulus + if modulus not in self._client_id or self._client_id[modulus] != client_id: + self.logger.info( + 'Received client ID: \n\n%s\n', + dumps(self.__client_info(client_id), beauty=True) + ) + self._client_id[modulus] = client_id + elif client_id.type == ClientIdentification.TokenType.KEYBOX: + # For keybox-based provisioning, register the token as the device ID + self.set_device_id(client_id.token) + elif client_id.type == ClientIdentification.TokenType.OEM_DEVICE_CERTIFICATE: + # Load the certificate chain from DER-encoded PKCS7 token + certs = pkcs7.load_der_pkcs7_certificates(data=client_id.token) + + # Validate the certificate chain length (expect exactly two certs) + if len(certs) != 2: + raise ValueError(f'Invalid certificate chain length: expected 2, got {len(certs)}') + + # Extract RSA modulus from the intermediate certificate's public key + modulus = certs[1].public_key().public_numbers().n + + # Register new or updated OEM certificates keyed by modulus + if modulus not in self._certificate: + # openssl asn1parse -inform der -in oem_certificate.der + # openssl pkcs7 -inform der -in oem_certificate.der -print_certs -out output.pem + self.logger.info( + 'Received OEM certificate: \n\n%s\n', + dumps([{ + 'subject': c.subject.rfc4514_string(), + 'issuer': c.subject.rfc4514_string(), + 'serial_number': c.serial_number, + 'valid_from': c.not_valid_before_utc, + 'valid_until': c.not_valid_after_utc + } for c in certs], beauty=True) + ) + self._certificate[modulus] = certs + else: + raise ValueError(f'Unsupported client type: {client_id.type}') + except Exception as e: + # Log exceptions at debug level to keep regular logs clean + self.logger.debug('Unable to register client ID: %s', e) + + def set_private_key(self, data: Union[bytes, List[Path]], name: Optional[str] = None) -> None: + """ + Registers an OEM private RSA key used for Widevine decryption. + Supports loading from raw PEM/DER bytes or multiple files containing keys. + + Args: + data (Union[bytes, List[Path]]): + Raw private key bytes in PEM or DER format, or a list of file paths to key files. + name (Optional[str]): + Optional identifier for the function or implementation related to this key. + """ + # If input is a list of file paths, process each file recursively + if isinstance(data, list): + for path in data: + try: + # Read the file's bytes and call this method again with raw bytes + self.set_private_key(data=path.read_bytes(), name=None) + except Exception as e: + self.logger.error('Could not load RSA key from file %s: %s', path, e) + + # If the input is not bytes at this point, do nothing (invalid input) + if not isinstance(data, bytes): + return + + try: + # Attempt to load the private key assuming PEM (Base64 encoded) format first + try: + private_key = serialization.load_pem_private_key( + data=data, + password=None, + backend=default_backend() + ) + except ValueError: + # If PEM loading fails, try DER (binary) format instead + private_key = serialization.load_der_private_key( + data=data, + password=None, + backend=default_backend() + ) + + # Extract the RSA modulus (n) from the public key for unique identification + modulus = private_key.public_key().public_numbers().n + + # Only register the key if this modulus is not already present + if modulus not in self._private_key: + self.logger.info( + 'Received RSA private key: \n\n%s\n', + private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ).decode('utf-8')) + self._private_key[modulus] = private_key + + # If a name is provided but unrecognized, log a warning for developer attention + if name and name not in OEM_CRYPTO_API: + self.logger.warning( + 'Unrecognized function name "%s". Please notify the developers to improve this tool.', + name + ) + except Exception as e: + # Log loading failures at debug level to avoid cluttering normal logs + self.logger.debug('Unable to register RSA private key: %s', e) + + def set_keybox(self, data: bytes) -> None: + """ + Parses and registers a KeyBox structure used for device provisioning. + The KeyBox contains cryptographic and device-specific data necessary for DRM. + + Args: + data (bytes): Raw binary data containing the serialized KeyBox protobuf. + """ + try: + # Initialize a new KeyBox instance and parse the protobuf data from bytes + keybox = KeyBox() + keybox.ParseFromString(data) # Parse the raw data into the KeyBox object + + # Check if the stable_id is either not present in the dictionary, + # or if the existing KeyBox with this stable_id does not have a device_id set. + if keybox.stable_id not in self._keybox or not self._keybox[keybox.stable_id].device_id: + # Extract keybox metadata info for logging + infos = keybox.keybox_info + # Determine label based on presence of system_id in the keybox info + label = 'keybox' if infos.get('system_id') else 'encrypted keybox' + + self.logger.info( + 'Received %s: \n\n%s\n', + label, dumps(infos, beauty=True) + ) + + # Store or update the internal keybox dictionary using stable_id as the key + self._keybox[keybox.stable_id] = keybox + except Exception as e: + # Log failure details at debug level to avoid noise in normal logs + self.logger.debug('Unable to register KeyBox data: %s', e) + + def set_stable_id(self, data: bytes) -> None: + """ + Registers or updates the stable ID for a KeyBox instance. + The stable ID uniquely identifies a device and acts as the key + in the internal KeyBox dictionary. + + Args: + data (bytes): Stable device identifier as raw bytes, + used as the unique key to identify the keybox. + + Notes: + - If a KeyBox already exists with an empty stable_id, it will be reused; + otherwise, a new KeyBox instance is created. + """ + try: + # Only proceed if this stable ID is not already present in the keybox dictionary + if data not in self._keybox: + # Retrieve existing KeyBox with empty stable_id or create a new instance + keybox = self._keybox.get(b'', KeyBox()) + + # Assign the provided stable ID to this KeyBox instance + keybox.stable_id = data + + # Log the stable ID in a human-readable ASCII-safe format + self.logger.info( + 'Received stable ID: \n\n%s\n', + encode_basestring_ascii(keybox.stable_id.decode('utf-8'))) + + # Store or update the KeyBox in the dictionary with stable_id as the key + self._keybox[keybox.stable_id] = keybox + except Exception as e: + # Log exception details at debug level without interrupting flow + self.logger.debug('Unable to register KeyBox stable ID: %s', e) + + def set_device_id(self, data: bytes) -> None: + """ + Registers or updates the device ID within a KeyBox instance. + The device ID is used to uniquely identify a device for cryptographic + provisioning and licensing purposes. + + Args: + data (bytes): Raw device identifier bytes, used to associate a device ID + with a KeyBox object. + + Notes: + - If the device ID is already associated with a KeyBox, this method does nothing. + - If a KeyBox without a device ID exists, it will be reused; otherwise, + a new KeyBox instance will be created. + """ + try: + # Check if this device ID is already registered in any existing KeyBox + if not any(k for k in self._keybox.values() if k.device_id == data): + # Find a KeyBox without a device ID to reuse, or create a new KeyBox + keybox = next((k for k in self._keybox.values() if not k.device_id), KeyBox()) + + # Assign the provided device ID to the KeyBox + keybox.device_id = data + + # Retrieve device-related metadata for logging purposes + infos = keybox.device_info + # Choose label based on whether a system_id is present in metadata + label = 'device ID' if infos.get('system_id') else 'encrypted device ID' + self.logger.info( + 'Received %s: \n\n%s\n', + label, dumps(infos, beauty=True) if infos else b64enc(keybox.device_id)) + + # Store or update the KeyBox in the dictionary using its stable_id as the key + self._keybox[keybox.stable_id] = keybox + except Exception as e: + # Log exceptions with context at debug level without raising + self.logger.debug('Unable to register KeyBox device ID: %s', e) + + def set_device_aes_key(self, data: Union[bytes, List[str]]) -> None: + """ + Imports and stores AES device keys used for decrypting protected content. + + This method accepts either a single AES key as raw bytes or a list of strings + representing AES keys in various formats, including: + - Hexadecimal strings + - Base64-encoded strings (standard or URL-safe) + - File paths pointing to binary key files + + Args: + data (bytes or List[str]): The AES key(s) to import. Either a single + 16-byte AES key (bytes) or a list of strings + representing keys in different encoded forms or file paths. + + Notes: + - Only keys exactly 16 bytes in length are accepted as valid AES keys. + - If a list of strings is given, the method tries each parser (hex, base64, file) + until one succeeds for each entry. + """ + if isinstance(data, list): + # Iterate over all string inputs, attempt parsing with multiple strategies + for value in data: + for parser in (bytes.fromhex, b64dec, lambda v: Path(v).read_bytes()): + try: + # Recursively call with successfully decoded raw bytes + self.set_device_aes_key(parser(value)) + break # Exit parser loop once successful + except Exception: + # Ignore failures and try next parser + pass + else: + # If no parser succeeded, log the failure with the raw input value + self.logger.error('Could not import AES key from input: %s', value) + + # From here on, ensure input is raw bytes representing the AES key + if not isinstance(data, bytes): + return + + try: + # Confirm the AES key length matches 16 bytes (AES-128 standard) + assert len(data) == 16, f'Invalid AES key length: expected 16 bytes, got {len(data)}' + + # Store the valid AES key into internal cache for later use + self._device_aes_key.append(data) + + # Log success message with the hex-encoded key for clarity + self.logger.info('Received device AES key: %s', data.hex()) + except Exception as e: + # Log any exceptions encountered during key validation or storage + self.logger.debug('Unable to register device AES key: %s', e) + + def set_provisioning_method(self, data: bytes) -> None: + """ + Determines and logs the provisioning method used by the Content Decryption Module (CDM). + + This method decodes the given byte data to an integer, which maps to an + OEMCrypto_ProvisioningMethod enumeration value. It helps identify the provisioning + mechanism in use and logs relevant diagnostic information, especially when L1 + provisioning appears to be disabled improperly. + + Args: + data (bytes): UTF-8 encoded string representing an integer corresponding to + an OEMCrypto_ProvisioningMethod enum value. + + Exception: + Catches and logs all exceptions encountered during decoding or enum conversion. + """ + try: + # Decode bytes to UTF-8 string, convert to int, and map to provisioning method enum + method = OEMCrypto_ProvisioningMethod(int(data.decode('utf-8'))) + if method == OEMCrypto_ProvisioningMethod.Keybox and self._disabler: + # Warn user if L1 provisioning is enabled but disabling procedure incomplete + self.logger.warning( + 'L1 provisioning deactivation appears incomplete. ' + 'Consider using a web dump or forcibly terminating the process to ensure proper disabling.' + ) + else: + # Log the provisioning method name for informational purposes + self.logger.debug('Receive provisioning method: %s', method.name) + except Exception as e: + # Log any errors during decoding or mapping to enum with debug severity + self.logger.debug('Unable to parse provisioning method: %s', e) + + def set_provisioning_response(self, data: bytes) -> None: + """ + Parses and applies a provisioning response from the Google Widevine provisioning service. + + Supports both Keybox-based provisioning and Provisioning 3.0 OTA PKI formats. + This method extracts and decrypts the device RSA private key using known AES session keys, + which may be retrieved from keyboxes or OEM provisioning keys. It also supports + setting up the device certificate and updating client identification. + + Args: + data (bytes): JSON-encoded provisioning response as received from the provisioning server. + + Exception: + Catches and logs all exceptions raised during parsing, decryption, or client ID setup. + """ + try: + # Extract and decode the base64-encoded signed provisioning message from the JSON response + b64_signed_data = json.loads(data.split(b'\x00')[0])['signedResponse'] + signed_data = b64dec(b64_signed_data, safe=True) + + # Parse the SignedProvisioningMessage protobuf + signed_response = SignedProvisioningMessage() + signed_response.ParseFromString(signed_data) + + # Extract the ProvisioningResponse payload embedded in the signed message + provisioning_response = ProvisioningResponse() + provisioning_response.ParseFromString(signed_response.message) + + # Gather all known AES session keys from stored keyboxes (OEM Keybox keys) + session_enc_keys = [k.device_aes_key for k in self._keybox.values() if k.device_aes_key] + # Optionally, add additional AES keys extracted via reverse engineering/TEE exploit (if any) + session_enc_keys += self._device_aes_key + + if provisioning_response.wrapping_key: + # OTA PKI-Based provisioning (Provisioning 3.0) + self.logger.info( + 'Received OTA provisioning response: \n\n%s\n', + dumps({ + 'signature': {'type': 'RSASSA-PSS', 'data': signed_response.signature}, + 'nonce': provisioning_response.nonce, + 'wrapping_key': provisioning_response.wrapping_key + }, beauty=True) + ) + + # Attempt to decrypt the AES wrapping key using all stored OEM private RSA keys + for oem_cert_priv_key in self._private_key.values(): + try: + session_enc_key = oem_cert_priv_key.decrypt( + ciphertext=provisioning_response.wrapping_key, + padding=OAEP( + mgf=MGF1(algorithm=SHA1()), + algorithm=SHA1(), + label=None + ) + ) + + session_enc_keys.append(session_enc_key) + except Exception as e: + self.logger.debug('Unable to decrypt OTA session key: %s', e) + else: + # Keybox-based provisioning (Provisioning 2.0) + self.logger.info( + 'Receive Keybox provisioning response: \n\n%s\n', + dumps({ + 'signature': {'type': 'HMAC-SHA256', 'data': signed_response.signature}, + 'nonce': provisioning_response.nonce + }, beauty=True) + ) + + # Attempt to decrypt the device RSA private key using all gathered AES session keys + for session_enc_key in session_enc_keys: + try: + # TODO: complete the keybox with the AES key + # Initialize AES-CBC cipher with the session key and provided IV + cipher = Cipher( + algorithm=AES(session_enc_key), + mode=CBC(provisioning_response.device_rsa_key_iv), + backend=default_backend() + ) + + decryptor = cipher.decryptor() + dec_padded_data = decryptor.update(provisioning_response.device_rsa_key) + decryptor.finalize() + + # Remove PKCS7 padding to obtain the original private key bytes + unpadder = PKCS7(AES.block_size).unpadder() + dec_data = unpadder.update(dec_padded_data) + unpadder.finalize() + + # Register the decrypted RSA private key internally + self.set_private_key(dec_data, None) + + # Successfully decrypted and stored the RSA key, no need to try further keys + # break + except Exception as e: + # If no decryption succeeded, OEM private key remains unset + # At this point, OTA provisioning using Provisioning 3.0 (PKI-based) might apply + # But this flow assumes Provisioning 2.0 (Keybox-based), so no further action here + self.logger.debug('Failed to decrypt RSA private key using AES key: %s', session_enc_key.hex()) + + # If a provisioning context exists, update the ClientIdentification with new capabilities and certificates + if self._provisioning: + """ + client_capabilities { + client_token: true + session_token: true + max_hdcp_version: HDCP_V2_2 + oem_crypto_api_version: 15 + anti_rollback_usage_table: false + srm_version: 0 + can_update_srm: false + supported_certificate_key_type: RSA_2048 + analog_output_capabilities: ANALOG_OUTPUT_NONE + can_disable_analog_output: false + } + """ + # Update client capabilities with typical fields + client_capabilities = self._provisioning.client_capabilities + client_capabilities.session_token = True + client_capabilities.max_hdcp_version = ClientIdentification.ClientCapabilities.HdcpVersion.HDCP_NONE + client_capabilities.anti_rollback_usage_table = False + client_capabilities.can_update_srm = False + + # Construct a new ClientIdentification token with the provisioned device certificate + client_id = ClientIdentification( + type=ClientIdentification.TokenType.DRM_DEVICE_CERTIFICATE, + token=provisioning_response.device_certificate, + client_info=self._provisioning.client_info, + provider_client_token=self._provisioning.provider_client_token, + license_counter=self._provisioning.license_counter, + client_capabilities=client_capabilities, + vmp_data=self._provisioning.vmp_data, + device_credentials=self._provisioning.device_credentials + ) + + # Register the updated client identification token + self.set_client_id(client_id) + except Exception as e: + # Log any unexpected error encountered during the provisioning response handling + self.logger.debug('Unable to process provisioning response: %s', e) + + def __resolve(self) -> Dict[int, dict]: + """ + Consolidates and organizes device-related information into a structured dictionary. + + This method collects client IDs, private keys, OEM certificates, and keyboxes, + grouping them by their system ID. It builds a comprehensive mapping containing: + - The file system path derived from client info (company and model names), + - The security level of the device (defaulting to level 3 if unknown), + - Lists of associated device keys, + - Linked keybox identifiers, + - Certificates associated with each system ID. + + The collected information is primarily used for managing device security contexts + such as DRM key provisioning and secure storage. + + Returns: + dict[int, dict]: Mapping of system IDs to dictionaries with device information. + + Notes: + - If no client info is available, the returned dictionary may be empty. + - Items are updated in multiple passes: first by client ID, then by certificates, and finally by keyboxes. + - Handles missing or incomplete data gracefully by skipping problematic entries. + """ + items = {} + path = None # Will hold the base path for device records derived from client info + + # Process all client IDs and their associated private keys + for key, client_id in self._client_id.items(): + # Parse DRM certificate from the client token protobuf data + signed_drm_certificate = SignedDrmCertificate() + signed_drm_certificate.ParseFromString(client_id.token) + + drm_certificate = DrmCertificate() + drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate) + + system_id = drm_certificate.system_id + + # Derive a filesystem path from client info on the first iteration + if not path: + client_info = self.__client_info(client_id) + path = Path() / unidec(client_info['company_name']) / unidec(client_info['model_name']) + + # Attempt to find the private key matching the current client key + private_key = next((v for k, v in self._private_key.items() if k == key), None) + + # Initialize or retrieve existing record for this system ID + item = items.get(system_id, { + 'path': path, + 'level': 3, # Default security level + 'device': [], # List of device keys associated + 'keybox': None, # Associated keybox ID + 'certificate': None # Associated certificate ID + }) + + # Add the client key to the device list if private key exists and not already added + if private_key and key not in item['device']: + item['device'].append(key) + items[system_id] = item + + # If no path was established from client IDs, return early with what we have + if not path: + return items + + # Add OEM device certificates to the records + for key, certs in self._certificate.items(): + intermediate_cert = certs[1] # Typically the intermediate cert in chain + + # Extract system ID and security level from certificate + system_id = CryptoSession_ExtractSystemIdFromOemCert(intermediate_cert) + level = CryptoSession_GetSecurityLevel(intermediate_cert) or 3 # Default to level 3 if unknown + + # Find matching private key for this certificate key + private_key = next((v for k, v in self._private_key.items() if k == key), None) + + # Initialize or retrieve record for this system ID + item = items.get(system_id, { + 'path': path, + 'level': 3, + 'device': [], + 'keybox': None, + 'certificate': None + }) + item['level'] = level + + # Assign the certificate key if private key exists and certificate is not already set + if private_key and item['certificate'] != key: + item['certificate'] = key + items[system_id] = item + + # Add keybox entries to the records + for key, keybox in self._keybox.items(): + try: + # Verify keybox completeness by serializing (throws if invalid) + keybox.SerializeToString() + + # Extract system ID from keybox info, if available + keybox_info = keybox.keybox_info + system_id = keybox_info.get('system_id') + + if system_id: + # Determine security level based on SDK version and keybox state + level = 1 if self._sdk > KEYBOX_MAX_CLEAR_API else 3 + + # Initialize or retrieve record for this system ID + item = items.get(system_id, { + 'path': path, + 'level': level, + 'device': [], + 'keybox': None, + 'certificate': None + }) + else: + # If system ID is missing, pick the first level 3 record as fallback + system_id, item = next((k, v) for k, v in items.items() if v['level'] == 3) + + # Link the keybox to the record if not already assigned + if item['keybox'] != key: + item['keybox'] = key + items[system_id] = item + except (AssertionError, StopIteration): + # Skip incomplete or invalid keybox entries silently + pass + + return items + + def export(self, wvd: bool = False, keybox: bool = False) -> Dict[Path, bytes]: + """ + Export device credentials and optionally serialize them into .wvd and keybox formats. + + This function collects all device-related data such as client IDs, private keys, + OEM certificates, and keyboxes, then prepares them for export by organizing + them into files mapped by file system paths. + + Args: + wvd (bool): If True, export device credentials in the pywidevine .wvd format, + enabling compatibility with the pywidevine toolset. + keybox (bool): If True, include OEM certificates and keybox binary data in the export. + + Returns: + dict[Path, bytes]: A dictionary mapping file paths (as Path objects) to their corresponding + binary content (as bytes), ready to be written to disk. + + Notes: + - File paths are constructed hierarchically based on company name, model name, system ID, and modulus. + - The WVD file names include company/model information and a CRC32 hash for uniqueness. + - OEM certificates and keyboxes are included only if `keybox` is set to True. + - Private keys are serialized in PEM format without encryption for direct use. + """ + files = {} + + # Retrieve organized records containing device and certificate info + records = self.__resolve() + + # Iterate through each device record grouped by system ID + for system_id, record in records.items(): + # Base directory for this system ID + parent = record['path'] / str(system_id) + + # Export client IDs and their private keys + for modulus in record['device']: + path = parent / str(modulus)[:10] + client_id = self._client_id[modulus] + private_key = self._private_key[modulus] + + # Serialize the client ID protobuf to bytes + client_id_serialized = client_id.SerializeToString() + + # Serialize the private key to PEM format without encryption + private_key_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + + # Save raw client ID and private key files + files[path / 'client_id.bin'] = client_id_serialized + files[path / 'private_key.pem'] = private_key_pem + + if wvd: + # https://github.com/devine-dl/pywidevine/blob/master/pywidevine/main.py#L211 + # Build pywidevine Device object for WVD serialization + device = Device( + type_=DeviceTypes.ANDROID, + security_level=record['level'], + flags=None, + private_key=private_key, + client_id=client_id + ) + + # Serialize the device to pywidevine-compatible WVD format + device_serialized = device.dumps() + + # Create a descriptive and unique filename based on device info and a CRC32 hash + client_info = self.__client_info(client_id) + name = f"{client_info['company_name']} {client_info['model_name']}" + if client_info.get('widevine_cdm_version'): + name += f" {client_info['widevine_cdm_version']}" + # Append CRC32 hash of serialized device for uniqueness + name += f" {crc32(device_serialized).to_bytes(4, 'big').hex()}" + # Normalize name to lowercase with underscores + name = re.sub(r'\s+', '_', unidec(name).lower()) + + # Store the serialized WVD file with naming convention including system ID and security level + files[path / f'{name}_{system_id}_l{device.security_level}.wvd'] = device_serialized + + # Skip keybox and OEM certificate export if flag is False + if not keybox: + continue + + # Export keybox binary if available + keybox = self._keybox.get(record['keybox']) + if keybox: + # Choose file suffix based on presence of system_id in keybox info + suffix = 'bin' if keybox.keybox_info.get('system_id') else 'enc' + # files[parent / f"keybox_{system_id}_l{record['level']}.{suffix}"] = keybox.SerializeToString() + files[parent / f'keybox.{suffix}'] = keybox.SerializeToString() + + # Export OEM certificate chain in PEM format if available + certificate = self._certificate.get(record['certificate']) + if certificate: + # files[parent / f"oem_certificate_{system_id}_l{record['level']}.pem"] = b''.join([ + files[parent / 'oem_certificate.pem'] = b''.join([ + c.public_bytes(encoding=serialization.Encoding.PEM) + for c in certificate + ]) + + # Export OEM private key in PEM format + private_key = self._private_key[record['certificate']] + # files[parent / f"oem_private_key_{system_id}_l{record['level']}.pem"] = private_key.private_bytes( + files[parent / 'oem_private_key.pem'] = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + + return files + + +__all__ = ('Cdm',) diff --git a/keydive/drm/device.py b/keydive/drm/device.py new file mode 100644 index 0000000..79adb58 --- /dev/null +++ b/keydive/drm/device.py @@ -0,0 +1,113 @@ +from enum import Enum +from typing import Optional + +from construct import BitStruct, Bytes, Const +from construct import Enum as CEnum +from construct import Int8ub, Int16ub +from construct import Optional as COptional +from construct import Padded, Padding, Struct, this +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey + +from keydive.drm.protocol.license_pb2 import ClientIdentification + + +class DeviceTypes(Enum): + CHROME = 1 + ANDROID = 2 + + +class _Structures: + magic = Const(b"WVD") + + header = Struct( + "signature" / magic, + "version" / Int8ub + ) + + v2 = Struct( + "signature" / magic, + "version" / Const(2, Int8ub), + "type_" / CEnum( + Int8ub, + **{t.name: t.value for t in DeviceTypes} + ), + "security_level" / Int8ub, + "flags" / Padded(1, COptional(BitStruct( + # no per-device flags yet + Padding(8) + ))), + "private_key_len" / Int16ub, + "private_key" / Bytes(this.private_key_len), + "client_id_len" / Int16ub, + "client_id" / Bytes(this.client_id_len) + ) + + +class Device: + """ + A minimal implementation of a WVD (Widevine Device) structure, + modeled after the pywidevine library. + + This class provides a lightweight alternative to the full `pywidevine.Device`, + retaining only the essential fields for constructing and serializing Widevine device + blobs in version 2 format, without performing certificate validation or VMP handling. + """ + Structures = _Structures + supported_structure = Structures.v2 + + def __init__( + self, + type_: DeviceTypes, + security_level: int, + flags: Optional[dict], + private_key: RSAPrivateKey, + client_id: ClientIdentification, + ): + """ + Initialize the Device object. + + Args: + type_ (DeviceTypes or str): The type of device. + security_level (int): The DRM security level (e.g., 1, 3). + flags (Optional[dict]): Optional feature flags (reserved for future use). + private_key (RSAPrivateKey): The device's RSA private key. + client_id (ClientIdentification): The protobuf client identifier. + """ + self.type = DeviceTypes[type_] if isinstance(type_, str) else type_ + self.security_level = security_level + self.flags = flags or {} + self.private_key = private_key + self.client_id = client_id + + def dumps(self) -> bytes: + """ + Serialize the Device object into a WVD-compatible binary format. + + Returns: + bytes: A binary representation of the device data. + """ + # Serialize the client ID protobuf message + bin_client_id = self.client_id.SerializeToString() + + # Serialize the private RSA key to DER format + bin_private_key = self.private_key.private_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + + # Build the structured binary representation + return self.supported_structure.build(dict( + version=2, + type_=self.type.value, + security_level=self.security_level, + flags=self.flags, + private_key_len=len(bin_private_key), + private_key=bin_private_key, + client_id_len=len(bin_client_id), + client_id=bin_client_id + )) + + +__all__ = ('Device', 'DeviceTypes') diff --git a/keydive/drm/keybox.py b/keydive/drm/keybox.py new file mode 100644 index 0000000..003115e --- /dev/null +++ b/keydive/drm/keybox.py @@ -0,0 +1,243 @@ +from uuid import UUID + +from crccheck.crc import Crc32Mpeg2 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher +from cryptography.hazmat.primitives.ciphers.algorithms import AES +from cryptography.hazmat.primitives.ciphers.modes import ECB +from cryptography.hazmat.primitives.padding import PKCS7 + +# Convert bytes to a big-endian unsigned integer +bytes2int = lambda x: int.from_bytes(x, byteorder='big', signed=False) + + +class KeyBox: + """ + Represents a binary DRM keybox containing a device's unique identifiers and AES key. + + A KeyBox is composed of: + - A 32-byte stable ID + - A 16-byte AES key + - A 72-byte device ID blob (may contain encrypted metadata) + - A 4-byte 'kbox' tag + - A 4-byte CRC32 checksum + - [Optional] A 4-byte 'LVL1' tag (QSEE-specific) + + This class supports both parsing and serialization of this binary format. + """ + + MAGIC_TAG = b'kbox' # Magic tag required at byte offset 120–124 + LEVEL_TAG = b'LVL1' # Optional level tag used by QSEE environments (byte offset 128–132) + + def __init__(self): + self._stable_id = b'' # 32-byte stable device ID + self._device_aes_key = b'' # 16-byte AES encryption key + self._device_id = b'' # 72-byte encrypted or encoded device-specific ID blob + + @property + def stable_id(self) -> bytes: + """ + Returns the stable ID of the device (32 bytes). + + Returns: + bytes: The stable device ID. + """ + return self._stable_id + + @stable_id.setter + def stable_id(self, value: bytes) -> None: + """ + Sets the 32-byte stable ID of the device. + + Args: + value (bytes): A 32-byte stable ID. + + Raises: + AssertionError: If `value` is not exactly 32 bytes long. + """ + assert len(value) == 32, f'Invalid stable ID length: expected 32 bytes, got {len(value)}' + self._stable_id = value + + @property + def device_aes_key(self) -> bytes: + """ + Returns the AES encryption key used by the device (16 bytes). + + Returns: + bytes: The device AES key. + """ + return self._device_aes_key + + @device_aes_key.setter + def device_aes_key(self, value: bytes) -> None: + """ + Sets the 16-byte AES key for the device. + + Args: + value (bytes): A 16-byte AES key. + + Raises: + AssertionError: If `value` is not exactly 16 bytes long. + """ + assert len(value) == 16, f'Invalid AES key length: expected 16 bytes, got {len(value)}' + self._device_aes_key = value + + @property + def device_id(self) -> bytes: + """ + Returns the 72-byte device ID blob. + + Returns: + bytes: The device-specific identifier blob. + """ + return self._device_id + + @device_id.setter + def device_id(self, value: bytes) -> None: + """ + Sets the device ID blob (72 bytes). + + Args: + value (bytes): A 72-byte device ID. + + Raises: + AssertionError: If `value` is not exactly 72 bytes long. + """ + assert len(value) == 72, f'Invalid device ID length: expected 72 bytes, got {len(value)}' + self._device_id = value + + def ParseFromString(self, serialized: bytes) -> None: + """ + Parses and validates a binary keybox structure into its individual fields. + + The expected format is: + - 32 bytes: stable ID + - 16 bytes: AES key + - 72 bytes: device ID + - 4 bytes: keybox tag (must be "kbox") + - 4 bytes: CRC32 checksum (MPEG-2 variant) + - [Optional] 4 bytes: level tag ("LVL1") for QSEE devices + + Args: + serialized (bytes): Raw keybox data (128 or 132 bytes). + + Raises: + AssertionError: If data is malformed or fails validation. + """ + # https://github.com/zybpp/Python/tree/master/Python/keybox + # Size of the entire serialized keybox (should be either 128 bytes for standard keybox, + # or 132 bytes for QSEE variant which appends an additional 4-byte level tag) + size = len(serialized) + + # Validate that the size is either of the two expected formats + assert size in (128, 132), f'Invalid keybox size: expected 128 or 132 bytes, got {size}' + + # Slice fields from the input + self.stable_id = serialized[0:32] # Device's unique identifier (32 bytes) + self.device_aes_key = serialized[32:48] # Device cryptographic key (16 bytes) + self.device_id = serialized[48:120] # Token for device authentication (72 bytes) + + magic_tag = serialized[120:124] # Magic tag (4 bytes) + body_crc = bytes2int(serialized[124:128]) # CRC32 checksum (4 bytes) + level_tag = serialized[128:132] # Optional level tag (4 bytes) + + # Ensure the 4-byte magic tag is exactly "kbox" + assert magic_tag == self.MAGIC_TAG, 'Keybox tag mismatch: expected "kbox"' + + # Validate the checksum using CRC32 MPEG-2 algorithm on the first 124 bytes + # This ensures data integrity and prevents accidental corruption or tampering + assert body_crc == Crc32Mpeg2.calc(serialized[:124]), 'CRC32 validation failed' + + # If the optional QSEE tag is present, ensure it exactly matches "LVL1" + # This tag typically denotes that the keybox is trusted or verified by Qualcomm Secure Execution Environment + assert size == 128 or level_tag == self.LEVEL_TAG, 'Unexpected QSEE tag: expected "LVL1"' + + @property + def device_info(self) -> dict: + """ + Attempts to decode device_id contents into meaningful fields. + + The layout is decoded if the first 4 bytes (flag) are <= 10. + Otherwise, the format is assumed encrypted or proprietary. + + Returns: + dict: Decoded fields such as 'flag', 'system_id', 'provisioning_id', and 'encrypted_bits'. + Returns an empty dict if layout is unknown. + """ + # Interpret device_id fields if the flag is within expected range (<= 10) + flag = bytes2int(self.device_id[0:4]) + if not flag > 10: + infos = { + 'flag': flag, # Device flag (4 bytes) + 'system_id': bytes2int(self.device_id[4:8]), # System identifier (4 bytes) + 'provisioning_id': UUID(bytes_le=self.device_id[8:24]), # Provisioning UUID (16 bytes) + 'encrypted_bits': self.device_id[24:72] # Encrypted device-specific information (48 bytes) + } + else: + # Encrypted format or unknown layout; optionally decrypt if format is known + # https://github.com/ThatNotEasy/Parser-DRM/blob/main/modules/widevine.py#L84 + """ + # Padding to AES block size (16 bytes) + padder = PKCS7(AES.block_size).padder() + enc_padded_data = padder.update(self.device_id[0:4]) + padder.finalize() + + # Create AES-ECB cipher with device key + cipher = Cipher( + algorithm=AES(self.device_aes_key), + mode=ECB(), + backend=default_backend() + ) + + # Decrypt padded data + decryptor = cipher.decryptor() + dec_padded_data = decryptor.update(enc_padded_data) + decryptor.finalize() + + # Remove padding to get original data + unpadder = PKCS7(AES.block_size).unpadder() + dec_data = unpadder.update(dec_padded_data) + unpadder.finalize() + """ + infos = {} + + return infos + + @property + def keybox_info(self) -> dict: + """ + Extracts all readable keybox metadata for external consumption. + + Returns: + dict: A dictionary containing raw and interpreted keybox data. + """ + return { + 'stable_id': self.stable_id, + 'device_aes_key': self.device_aes_key, + 'device_id': self.device_id, + **self.device_info + } + + def SerializeToString(self) -> bytes: + """ + Serializes the current keybox structure to a binary blob. + + Returns: + bytes: A 128-byte serialized keybox with checksum. + + Raises: + AssertionError: If any required field is missing. + """ + # Ensure that all essential components are set; without them, a valid keybox cannot be constructed + assert self.stable_id and self.device_aes_key and self.device_id, 'Cannot serialize: one or more required fields are missing' + + # This forms the first 124 bytes of the final binary output. + serialized = self.stable_id + self.device_aes_key + self.device_id + self.MAGIC_TAG + + # Calculate a CRC32 checksum using the MPEG-2 variant on the first 124 bytes + # This helps verify data integrity when the structure is later read or validated + body_crc = Crc32Mpeg2.calc(serialized) + + # Convert the CRC integer into a 4-byte big-endian format and append it to the structure + # This completes the full 128-byte keybox format required by consumers or hardware modules + return serialized + int.to_bytes(body_crc, length=4, byteorder='big', signed=False) + + +__all__ = ('KeyBox',) diff --git a/keydive/drm/protocol/license_pb2.py b/keydive/drm/protocol/license_pb2.py new file mode 100644 index 0000000..5e3e30c --- /dev/null +++ b/keydive/drm/protocol/license_pb2.py @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: license.proto +# Protobuf Python Version: 5.29.5 +# Command: protoc -I. --python_out=. --mypy_out=. license.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rlicense.proto\x12\x18keydive_license_protocol\"\xb5\x02\n\x15LicenseIdentification\x12\x12\n\nrequest_id\x18\x01 \x01(\x0c\x12\x12\n\nsession_id\x18\x02 \x01(\x0c\x12\x13\n\x0bpurchase_id\x18\x03 \x01(\x0c\x12\x33\n\x04type\x18\x04 \x01(\x0e\x32%.keydive_license_protocol.LicenseType\x12\x0f\n\x07version\x18\x05 \x01(\x05\x12\x1e\n\x16provider_session_token\x18\x06 \x01(\x0c\x12(\n original_rental_duration_seconds\x18\x07 \x01(\x03\x12*\n\"original_playback_duration_seconds\x18\x08 \x01(\x03\x12#\n\x1boriginal_start_time_seconds\x18\t \x01(\x03\"\xfd\r\n\x0eLicenseRequest\x12\x41\n\tclient_id\x18\x01 \x01(\x0b\x32..keydive_license_protocol.ClientIdentification\x12R\n\ncontent_id\x18\x02 \x01(\x0b\x32>.keydive_license_protocol.LicenseRequest.ContentIdentification\x12\x42\n\x04type\x18\x03 \x01(\x0e\x32\x34.keydive_license_protocol.LicenseRequest.RequestType\x12\x14\n\x0crequest_time\x18\x04 \x01(\x03\x12$\n\x1ckey_control_nonce_deprecated\x18\x05 \x01(\x0c\x12P\n\x10protocol_version\x18\x06 \x01(\x0e\x32).keydive_license_protocol.ProtocolVersion:\x0bVERSION_2_0\x12\x19\n\x11key_control_nonce\x18\x07 \x01(\r\x12T\n\x13\x65ncrypted_client_id\x18\x08 \x01(\x0b\x32\x37.keydive_license_protocol.EncryptedClientIdentification\x12\x16\n\x0e\x63lient_version\x18\t \x01(\t\x1a\xf4\x08\n\x15\x43ontentIdentification\x12m\n\x12widevine_pssh_data\x18\x01 \x01(\x0b\x32O.keydive_license_protocol.LicenseRequest.ContentIdentification.WidevinePsshDataH\x00\x12_\n\x0bwebm_key_id\x18\x02 \x01(\x0b\x32H.keydive_license_protocol.LicenseRequest.ContentIdentification.WebmKeyIdH\x00\x12j\n\x10\x65xisting_license\x18\x03 \x01(\x0b\x32N.keydive_license_protocol.LicenseRequest.ContentIdentification.ExistingLicenseH\x00\x12\\\n\tinit_data\x18\x04 \x01(\x0b\x32G.keydive_license_protocol.LicenseRequest.ContentIdentification.InitDataH\x00\x1av\n\x10WidevinePsshData\x12\x11\n\tpssh_data\x18\x01 \x03(\x0c\x12;\n\x0clicense_type\x18\x02 \x01(\x0e\x32%.keydive_license_protocol.LicenseType\x12\x12\n\nrequest_id\x18\x03 \x01(\x0c\x1al\n\tWebmKeyId\x12\x0e\n\x06header\x18\x01 \x01(\x0c\x12;\n\x0clicense_type\x18\x02 \x01(\x0e\x32%.keydive_license_protocol.LicenseType\x12\x12\n\nrequest_id\x18\x03 \x01(\x0c\x1a\xbb\x01\n\x0f\x45xistingLicense\x12\x43\n\nlicense_id\x18\x01 \x01(\x0b\x32/.keydive_license_protocol.LicenseIdentification\x12\x1d\n\x15seconds_since_started\x18\x02 \x01(\x03\x12!\n\x19seconds_since_last_played\x18\x03 \x01(\x03\x12!\n\x19session_usage_table_entry\x18\x04 \x01(\x0c\x1a\x86\x02\n\x08InitData\x12r\n\x0einit_data_type\x18\x01 \x01(\x0e\x32T.keydive_license_protocol.LicenseRequest.ContentIdentification.InitData.InitDataType:\x04\x43\x45NC\x12\x11\n\tinit_data\x18\x02 \x01(\x0c\x12;\n\x0clicense_type\x18\x03 \x01(\x0e\x32%.keydive_license_protocol.LicenseType\x12\x12\n\nrequest_id\x18\x04 \x01(\x0c\"\"\n\x0cInitDataType\x12\x08\n\x04\x43\x45NC\x10\x01\x12\x08\n\x04WEBM\x10\x02\x42\x14\n\x12\x63ontent_id_variant\x1aP\n\x0eSubSessionData\x12\x1a\n\x12sub_session_key_id\x18\x01 \x01(\t\x12\r\n\x05nonce\x18\x02 \x01(\r\x12\x13\n\x0btrack_label\x18\x03 \x01(\t\"0\n\x0bRequestType\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07RELEASE\x10\x03\"\xed\x01\n\nMetricData\x12\x12\n\nstage_name\x18\x01 \x01(\t\x12\x43\n\x0bmetric_data\x18\x02 \x03(\x0b\x32..keydive_license_protocol.MetricData.TypeValue\x1a\\\n\tTypeValue\x12=\n\x04type\x18\x01 \x01(\x0e\x32/.keydive_license_protocol.MetricData.MetricType\x12\x10\n\x05value\x18\x02 \x01(\x03:\x01\x30\"(\n\nMetricType\x12\x0b\n\x07LATENCY\x10\x01\x12\r\n\tTIMESTAMP\x10\x02\"K\n\x0bVersionInfo\x12\x1b\n\x13license_sdk_version\x18\x01 \x01(\t\x12\x1f\n\x17license_service_version\x18\x02 \x01(\t\"\x8e\x06\n\rSignedMessage\x12\x41\n\x04type\x18\x01 \x01(\x0e\x32\x33.keydive_license_protocol.SignedMessage.MessageType\x12\x0b\n\x03msg\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x13\n\x0bsession_key\x18\x04 \x01(\x0c\x12\x1a\n\x12remote_attestation\x18\x05 \x01(\x0c\x12\x39\n\x0bmetric_data\x18\x06 \x03(\x0b\x32$.keydive_license_protocol.MetricData\x12\x43\n\x14service_version_info\x18\x07 \x01(\x0b\x32%.keydive_license_protocol.VersionInfo\x12\x61\n\x10session_key_type\x18\x08 \x01(\x0e\x32\x36.keydive_license_protocol.SignedMessage.SessionKeyType:\x0fWRAPPED_AES_KEY\x12\x1e\n\x16oemcrypto_core_message\x18\t \x01(\x0c\x12\"\n\x13using_secondary_key\x18\x0b \x01(\x08:\x05\x66\x61lse\"\xec\x01\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\x12\x0f\n\x0bSUB_LICENSE\x10\x06\x12\x17\n\x13\x43\x41S_LICENSE_REQUEST\x10\x07\x12\x0f\n\x0b\x43\x41S_LICENSE\x10\x08\x12\x1c\n\x18\x45XTERNAL_LICENSE_REQUEST\x10\t\x12\x14\n\x10\x45XTERNAL_LICENSE\x10\n\"S\n\x0eSessionKeyType\x12\r\n\tUNDEFINED\x10\x00\x12\x13\n\x0fWRAPPED_AES_KEY\x10\x01\x12\x1d\n\x19\x45PHERMERAL_ECC_PUBLIC_KEY\x10\x02\"\xf0\x03\n\x14ProvisioningResponse\x12\x16\n\x0e\x64\x65vice_rsa_key\x18\x01 \x01(\x0c\x12\x19\n\x11\x64\x65vice_rsa_key_iv\x18\x02 \x01(\x0c\x12\x1a\n\x12\x64\x65vice_certificate\x18\x03 \x01(\x0c\x12\r\n\x05nonce\x18\x04 \x01(\x0c\x12\x14\n\x0cwrapping_key\x18\x05 \x01(\x0c\x12L\n\nota_keybox\x18\x06 \x01(\x0b\x32\x38.keydive_license_protocol.ProvisioningResponse.OtaKeybox\x12Q\n\x06status\x18\x07 \x01(\x0e\x32\x41.keydive_license_protocol.ProvisioningResponse.ProvisioningStatus\x1a\x64\n\tOtaKeybox\x12 \n\x18\x64\x65vice_key_encryption_iv\x18\x01 \x01(\x0c\x12\x1c\n\x14\x65ncrypted_device_key\x18\x02 \x01(\x0c\x12\x17\n\x0f\x64\x65vice_ca_token\x18\x03 \x01(\x0c\"]\n\x12ProvisioningStatus\x12\x0c\n\x08NO_ERROR\x10\x00\x12\x1e\n\x1aREVOKED_DEVICE_CREDENTIALS\x10\x01\x12\x19\n\x15REVOKED_DEVICE_SERIES\x10\x02\"L\n\x19SignedProvisioningContext\x12\x1c\n\x14provisioning_context\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\"\x8f\x06\n\x19SignedProvisioningMessage\x12\x0f\n\x07message\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12p\n\x11provisioning_type\x18\x03 \x01(\x0e\x32\x44.keydive_license_protocol.SignedProvisioningMessage.ProvisioningType:\x0fPROVISIONING_20\x12X\n\x1bsigned_provisioning_context\x18\x04 \x01(\x0b\x32\x33.keydive_license_protocol.SignedProvisioningContext\x12\x1a\n\x12remote_attestation\x18\x05 \x01(\x0c\x12\x1e\n\x16oemcrypto_core_message\x18\x06 \x01(\x0c\x12\x44\n\x0ehash_algorithm\x18\x07 \x01(\x0e\x32,.keydive_license_protocol.HashAlgorithmProto\x12i\n\x10protocol_version\x18\x08 \x01(\x0e\x32O.keydive_license_protocol.SignedProvisioningMessage.ProvisioningProtocolVersion\"e\n\x1bProvisioningProtocolVersion\x12\x17\n\x13VERSION_UNSPECIFIED\x10\x00\x12\r\n\tVERSION_1\x10\x01\x12\x0f\n\x0bVERSION_1_1\x10\x02\x12\r\n\tVERSION_2\x10\x03\"\xad\x01\n\x10ProvisioningType\x12!\n\x1dPROVISIONING_TYPE_UNSPECIFIED\x10\x00\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x01\x12\x13\n\x0fPROVISIONING_20\x10\x02\x12\x13\n\x0fPROVISIONING_30\x10\x03\x12\x16\n\x12\x41RCPP_PROVISIONING\x10\x04\x12\x13\n\x0fINTEL_SIGMA_101\x10\x65\"\x81\x11\n\x14\x43lientIdentification\x12N\n\x04type\x18\x01 \x01(\x0e\x32\x38.keydive_license_protocol.ClientIdentification.TokenType:\x06KEYBOX\x12\r\n\x05token\x18\x02 \x01(\x0c\x12M\n\x0b\x63lient_info\x18\x03 \x03(\x0b\x32\x38.keydive_license_protocol.ClientIdentification.NameValue\x12\x1d\n\x15provider_client_token\x18\x04 \x01(\x0c\x12\x17\n\x0flicense_counter\x18\x05 \x01(\r\x12^\n\x13\x63lient_capabilities\x18\x06 \x01(\x0b\x32\x41.keydive_license_protocol.ClientIdentification.ClientCapabilities\x12\x10\n\x08vmp_data\x18\x07 \x01(\x0c\x12\\\n\x12\x64\x65vice_credentials\x18\x08 \x03(\x0b\x32@.keydive_license_protocol.ClientIdentification.ClientCredentials\x1a(\n\tNameValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x1a\x9f\x0b\n\x12\x43lientCapabilities\x12\x1b\n\x0c\x63lient_token\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x1c\n\rsession_token\x18\x02 \x01(\x08:\x05\x66\x61lse\x12+\n\x1cvideo_resolution_constraints\x18\x03 \x01(\x08:\x05\x66\x61lse\x12r\n\x10max_hdcp_version\x18\x04 \x01(\x0e\x32M.keydive_license_protocol.ClientIdentification.ClientCapabilities.HdcpVersion:\tHDCP_NONE\x12\x1e\n\x16oem_crypto_api_version\x18\x05 \x01(\r\x12(\n\x19\x61nti_rollback_usage_table\x18\x06 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x0bsrm_version\x18\x07 \x01(\r\x12\x1d\n\x0e\x63\x61n_update_srm\x18\x08 \x01(\x08:\x05\x66\x61lse\x12|\n\x1esupported_certificate_key_type\x18\t \x03(\x0e\x32T.keydive_license_protocol.ClientIdentification.ClientCapabilities.CertificateKeyType\x12\x95\x01\n\x1a\x61nalog_output_capabilities\x18\n \x01(\x0e\x32Z.keydive_license_protocol.ClientIdentification.ClientCapabilities.AnalogOutputCapabilities:\x15\x41NALOG_OUTPUT_UNKNOWN\x12(\n\x19\x63\x61n_disable_analog_output\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x14resource_rating_tier\x18\x0c \x01(\r:\x01\x30\x12\x8f\x01\n\x14watermarking_support\x18\r \x01(\x0e\x32U.keydive_license_protocol.ClientIdentification.ClientCapabilities.WatermarkingSupport:\x1aWATERMARKING_NOT_SUPPORTED\x12)\n\x1ainitial_renewal_delay_base\x18\x0e \x01(\x08:\x05\x66\x61lse\"\x80\x01\n\x0bHdcpVersion\x12\r\n\tHDCP_NONE\x10\x00\x12\x0b\n\x07HDCP_V1\x10\x01\x12\x0b\n\x07HDCP_V2\x10\x02\x12\r\n\tHDCP_V2_1\x10\x03\x12\r\n\tHDCP_V2_2\x10\x04\x12\r\n\tHDCP_V2_3\x10\x05\x12\x1b\n\x16HDCP_NO_DIGITAL_OUTPUT\x10\xff\x01\"i\n\x12\x43\x65rtificateKeyType\x12\x0c\n\x08RSA_2048\x10\x00\x12\x0c\n\x08RSA_3072\x10\x01\x12\x11\n\rECC_SECP256R1\x10\x02\x12\x11\n\rECC_SECP384R1\x10\x03\x12\x11\n\rECC_SECP521R1\x10\x04\"\x8d\x01\n\x18\x41nalogOutputCapabilities\x12\x19\n\x15\x41NALOG_OUTPUT_UNKNOWN\x10\x00\x12\x16\n\x12\x41NALOG_OUTPUT_NONE\x10\x01\x12\x1b\n\x17\x41NALOG_OUTPUT_SUPPORTED\x10\x02\x12!\n\x1d\x41NALOG_OUTPUT_SUPPORTS_CGMS_A\x10\x03\"\x92\x01\n\x13WatermarkingSupport\x12 \n\x1cWATERMARKING_SUPPORT_UNKNOWN\x10\x00\x12\x1e\n\x1aWATERMARKING_NOT_SUPPORTED\x10\x01\x12\x1d\n\x19WATERMARKING_CONFIGURABLE\x10\x02\x12\x1a\n\x16WATERMARKING_ALWAYS_ON\x10\x03\x1ar\n\x11\x43lientCredentials\x12N\n\x04type\x18\x01 \x01(\x0e\x32\x38.keydive_license_protocol.ClientIdentification.TokenType:\x06KEYBOX\x12\r\n\x05token\x18\x02 \x01(\x0c\"s\n\tTokenType\x12\n\n\x06KEYBOX\x10\x00\x12\x1a\n\x16\x44RM_DEVICE_CERTIFICATE\x10\x01\x12\"\n\x1eREMOTE_ATTESTATION_CERTIFICATE\x10\x02\x12\x1a\n\x16OEM_DEVICE_CERTIFICATE\x10\x03\"\xbb\x01\n\x1d\x45ncryptedClientIdentification\x12\x13\n\x0bprovider_id\x18\x01 \x01(\t\x12)\n!service_certificate_serial_number\x18\x02 \x01(\x0c\x12\x1b\n\x13\x65ncrypted_client_id\x18\x03 \x01(\x0c\x12\x1e\n\x16\x65ncrypted_client_id_iv\x18\x04 \x01(\x0c\x12\x1d\n\x15\x65ncrypted_privacy_key\x18\x05 \x01(\x0c\"\xab\x07\n\x0e\x44rmCertificate\x12;\n\x04type\x18\x01 \x01(\x0e\x32-.keydive_license_protocol.DrmCertificate.Type\x12\x15\n\rserial_number\x18\x02 \x01(\x0c\x12\x1d\n\x15\x63reation_time_seconds\x18\x03 \x01(\r\x12\x1f\n\x17\x65xpiration_time_seconds\x18\x0c \x01(\r\x12\x12\n\npublic_key\x18\x04 \x01(\x0c\x12\x11\n\tsystem_id\x18\x05 \x01(\r\x12\"\n\x16test_device_deprecated\x18\x06 \x01(\x08\x42\x02\x18\x01\x12\x13\n\x0bprovider_id\x18\x07 \x01(\t\x12K\n\rservice_types\x18\x08 \x03(\x0e\x32\x34.keydive_license_protocol.DrmCertificate.ServiceType\x12J\n\talgorithm\x18\t \x01(\x0e\x32\x32.keydive_license_protocol.DrmCertificate.Algorithm:\x03RSA\x12\x0e\n\x06rot_id\x18\n \x01(\x0c\x12N\n\x0e\x65ncryption_key\x18\x0b \x01(\x0b\x32\x36.keydive_license_protocol.DrmCertificate.EncryptionKey\x1ao\n\rEncryptionKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12J\n\talgorithm\x18\x02 \x01(\x0e\x32\x32.keydive_license_protocol.DrmCertificate.Algorithm:\x03RSA\"L\n\x04Type\x12\x08\n\x04ROOT\x10\x00\x12\x10\n\x0c\x44\x45VICE_MODEL\x10\x01\x12\n\n\x06\x44\x45VICE\x10\x02\x12\x0b\n\x07SERVICE\x10\x03\x12\x0f\n\x0bPROVISIONER\x10\x04\"\x86\x01\n\x0bServiceType\x12\x18\n\x14UNKNOWN_SERVICE_TYPE\x10\x00\x12\x16\n\x12LICENSE_SERVER_SDK\x10\x01\x12\x1c\n\x18LICENSE_SERVER_PROXY_SDK\x10\x02\x12\x14\n\x10PROVISIONING_SDK\x10\x03\x12\x11\n\rCAS_PROXY_SDK\x10\x04\"d\n\tAlgorithm\x12\x15\n\x11UNKNOWN_ALGORITHM\x10\x00\x12\x07\n\x03RSA\x10\x01\x12\x11\n\rECC_SECP256R1\x10\x02\x12\x11\n\rECC_SECP384R1\x10\x03\x12\x11\n\rECC_SECP521R1\x10\x04\"\xc8\x01\n\x14SignedDrmCertificate\x12\x17\n\x0f\x64rm_certificate\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12>\n\x06signer\x18\x03 \x01(\x0b\x32..keydive_license_protocol.SignedDrmCertificate\x12\x44\n\x0ehash_algorithm\x18\x04 \x01(\x0e\x32,.keydive_license_protocol.HashAlgorithmProto*8\n\x0bLicenseType\x12\r\n\tSTREAMING\x10\x01\x12\x0b\n\x07OFFLINE\x10\x02\x12\r\n\tAUTOMATIC\x10\x03*D\n\x0fProtocolVersion\x12\x0f\n\x0bVERSION_2_0\x10\x14\x12\x0f\n\x0bVERSION_2_1\x10\x15\x12\x0f\n\x0bVERSION_2_2\x10\x16*\x86\x01\n\x12HashAlgorithmProto\x12\x1e\n\x1aHASH_ALGORITHM_UNSPECIFIED\x10\x00\x12\x18\n\x14HASH_ALGORITHM_SHA_1\x10\x01\x12\x1a\n\x16HASH_ALGORITHM_SHA_256\x10\x02\x12\x1a\n\x16HASH_ALGORITHM_SHA_384\x10\x03\x42\x1e\n\x1a\x63om.keydive.license.protosH\x03') + +_LICENSETYPE = DESCRIPTOR.enum_types_by_name['LicenseType'] +LicenseType = enum_type_wrapper.EnumTypeWrapper(_LICENSETYPE) +_PROTOCOLVERSION = DESCRIPTOR.enum_types_by_name['ProtocolVersion'] +ProtocolVersion = enum_type_wrapper.EnumTypeWrapper(_PROTOCOLVERSION) +_HASHALGORITHMPROTO = DESCRIPTOR.enum_types_by_name['HashAlgorithmProto'] +HashAlgorithmProto = enum_type_wrapper.EnumTypeWrapper(_HASHALGORITHMPROTO) +STREAMING = 1 +OFFLINE = 2 +AUTOMATIC = 3 +VERSION_2_0 = 20 +VERSION_2_1 = 21 +VERSION_2_2 = 22 +HASH_ALGORITHM_UNSPECIFIED = 0 +HASH_ALGORITHM_SHA_1 = 1 +HASH_ALGORITHM_SHA_256 = 2 +HASH_ALGORITHM_SHA_384 = 3 + + +_LICENSEIDENTIFICATION = DESCRIPTOR.message_types_by_name['LicenseIdentification'] +_LICENSEREQUEST = DESCRIPTOR.message_types_by_name['LicenseRequest'] +_LICENSEREQUEST_CONTENTIDENTIFICATION = _LICENSEREQUEST.nested_types_by_name['ContentIdentification'] +_LICENSEREQUEST_CONTENTIDENTIFICATION_WIDEVINEPSSHDATA = _LICENSEREQUEST_CONTENTIDENTIFICATION.nested_types_by_name['WidevinePsshData'] +_LICENSEREQUEST_CONTENTIDENTIFICATION_WEBMKEYID = _LICENSEREQUEST_CONTENTIDENTIFICATION.nested_types_by_name['WebmKeyId'] +_LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE = _LICENSEREQUEST_CONTENTIDENTIFICATION.nested_types_by_name['ExistingLicense'] +_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA = _LICENSEREQUEST_CONTENTIDENTIFICATION.nested_types_by_name['InitData'] +_LICENSEREQUEST_SUBSESSIONDATA = _LICENSEREQUEST.nested_types_by_name['SubSessionData'] +_METRICDATA = DESCRIPTOR.message_types_by_name['MetricData'] +_METRICDATA_TYPEVALUE = _METRICDATA.nested_types_by_name['TypeValue'] +_VERSIONINFO = DESCRIPTOR.message_types_by_name['VersionInfo'] +_SIGNEDMESSAGE = DESCRIPTOR.message_types_by_name['SignedMessage'] +_PROVISIONINGRESPONSE = DESCRIPTOR.message_types_by_name['ProvisioningResponse'] +_PROVISIONINGRESPONSE_OTAKEYBOX = _PROVISIONINGRESPONSE.nested_types_by_name['OtaKeybox'] +_SIGNEDPROVISIONINGCONTEXT = DESCRIPTOR.message_types_by_name['SignedProvisioningContext'] +_SIGNEDPROVISIONINGMESSAGE = DESCRIPTOR.message_types_by_name['SignedProvisioningMessage'] +_CLIENTIDENTIFICATION = DESCRIPTOR.message_types_by_name['ClientIdentification'] +_CLIENTIDENTIFICATION_NAMEVALUE = _CLIENTIDENTIFICATION.nested_types_by_name['NameValue'] +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES = _CLIENTIDENTIFICATION.nested_types_by_name['ClientCapabilities'] +_CLIENTIDENTIFICATION_CLIENTCREDENTIALS = _CLIENTIDENTIFICATION.nested_types_by_name['ClientCredentials'] +_ENCRYPTEDCLIENTIDENTIFICATION = DESCRIPTOR.message_types_by_name['EncryptedClientIdentification'] +_DRMCERTIFICATE = DESCRIPTOR.message_types_by_name['DrmCertificate'] +_DRMCERTIFICATE_ENCRYPTIONKEY = _DRMCERTIFICATE.nested_types_by_name['EncryptionKey'] +_SIGNEDDRMCERTIFICATE = DESCRIPTOR.message_types_by_name['SignedDrmCertificate'] +_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA_INITDATATYPE = _LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA.enum_types_by_name['InitDataType'] +_LICENSEREQUEST_REQUESTTYPE = _LICENSEREQUEST.enum_types_by_name['RequestType'] +_METRICDATA_METRICTYPE = _METRICDATA.enum_types_by_name['MetricType'] +_SIGNEDMESSAGE_MESSAGETYPE = _SIGNEDMESSAGE.enum_types_by_name['MessageType'] +_SIGNEDMESSAGE_SESSIONKEYTYPE = _SIGNEDMESSAGE.enum_types_by_name['SessionKeyType'] +_PROVISIONINGRESPONSE_PROVISIONINGSTATUS = _PROVISIONINGRESPONSE.enum_types_by_name['ProvisioningStatus'] +_SIGNEDPROVISIONINGMESSAGE_PROVISIONINGPROTOCOLVERSION = _SIGNEDPROVISIONINGMESSAGE.enum_types_by_name['ProvisioningProtocolVersion'] +_SIGNEDPROVISIONINGMESSAGE_PROVISIONINGTYPE = _SIGNEDPROVISIONINGMESSAGE.enum_types_by_name['ProvisioningType'] +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES.enum_types_by_name['HdcpVersion'] +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES.enum_types_by_name['CertificateKeyType'] +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_ANALOGOUTPUTCAPABILITIES = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES.enum_types_by_name['AnalogOutputCapabilities'] +_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_WATERMARKINGSUPPORT = _CLIENTIDENTIFICATION_CLIENTCAPABILITIES.enum_types_by_name['WatermarkingSupport'] +_CLIENTIDENTIFICATION_TOKENTYPE = _CLIENTIDENTIFICATION.enum_types_by_name['TokenType'] +_DRMCERTIFICATE_TYPE = _DRMCERTIFICATE.enum_types_by_name['Type'] +_DRMCERTIFICATE_SERVICETYPE = _DRMCERTIFICATE.enum_types_by_name['ServiceType'] +_DRMCERTIFICATE_ALGORITHM = _DRMCERTIFICATE.enum_types_by_name['Algorithm'] +LicenseIdentification = _reflection.GeneratedProtocolMessageType('LicenseIdentification', (_message.Message,), { + 'DESCRIPTOR' : _LICENSEIDENTIFICATION, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.LicenseIdentification) + }) +_sym_db.RegisterMessage(LicenseIdentification) + +LicenseRequest = _reflection.GeneratedProtocolMessageType('LicenseRequest', (_message.Message,), { + + 'ContentIdentification' : _reflection.GeneratedProtocolMessageType('ContentIdentification', (_message.Message,), { + + 'WidevinePsshData' : _reflection.GeneratedProtocolMessageType('WidevinePsshData', (_message.Message,), { + 'DESCRIPTOR' : _LICENSEREQUEST_CONTENTIDENTIFICATION_WIDEVINEPSSHDATA, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.LicenseRequest.ContentIdentification.WidevinePsshData) + }) + , + + 'WebmKeyId' : _reflection.GeneratedProtocolMessageType('WebmKeyId', (_message.Message,), { + 'DESCRIPTOR' : _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBMKEYID, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.LicenseRequest.ContentIdentification.WebmKeyId) + }) + , + + 'ExistingLicense' : _reflection.GeneratedProtocolMessageType('ExistingLicense', (_message.Message,), { + 'DESCRIPTOR' : _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.LicenseRequest.ContentIdentification.ExistingLicense) + }) + , + + 'InitData' : _reflection.GeneratedProtocolMessageType('InitData', (_message.Message,), { + 'DESCRIPTOR' : _LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.LicenseRequest.ContentIdentification.InitData) + }) + , + 'DESCRIPTOR' : _LICENSEREQUEST_CONTENTIDENTIFICATION, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.LicenseRequest.ContentIdentification) + }) + , + + 'SubSessionData' : _reflection.GeneratedProtocolMessageType('SubSessionData', (_message.Message,), { + 'DESCRIPTOR' : _LICENSEREQUEST_SUBSESSIONDATA, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.LicenseRequest.SubSessionData) + }) + , + 'DESCRIPTOR' : _LICENSEREQUEST, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.LicenseRequest) + }) +_sym_db.RegisterMessage(LicenseRequest) +_sym_db.RegisterMessage(LicenseRequest.ContentIdentification) +_sym_db.RegisterMessage(LicenseRequest.ContentIdentification.WidevinePsshData) +_sym_db.RegisterMessage(LicenseRequest.ContentIdentification.WebmKeyId) +_sym_db.RegisterMessage(LicenseRequest.ContentIdentification.ExistingLicense) +_sym_db.RegisterMessage(LicenseRequest.ContentIdentification.InitData) +_sym_db.RegisterMessage(LicenseRequest.SubSessionData) + +MetricData = _reflection.GeneratedProtocolMessageType('MetricData', (_message.Message,), { + + 'TypeValue' : _reflection.GeneratedProtocolMessageType('TypeValue', (_message.Message,), { + 'DESCRIPTOR' : _METRICDATA_TYPEVALUE, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.MetricData.TypeValue) + }) + , + 'DESCRIPTOR' : _METRICDATA, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.MetricData) + }) +_sym_db.RegisterMessage(MetricData) +_sym_db.RegisterMessage(MetricData.TypeValue) + +VersionInfo = _reflection.GeneratedProtocolMessageType('VersionInfo', (_message.Message,), { + 'DESCRIPTOR' : _VERSIONINFO, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.VersionInfo) + }) +_sym_db.RegisterMessage(VersionInfo) + +SignedMessage = _reflection.GeneratedProtocolMessageType('SignedMessage', (_message.Message,), { + 'DESCRIPTOR' : _SIGNEDMESSAGE, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.SignedMessage) + }) +_sym_db.RegisterMessage(SignedMessage) + +ProvisioningResponse = _reflection.GeneratedProtocolMessageType('ProvisioningResponse', (_message.Message,), { + + 'OtaKeybox' : _reflection.GeneratedProtocolMessageType('OtaKeybox', (_message.Message,), { + 'DESCRIPTOR' : _PROVISIONINGRESPONSE_OTAKEYBOX, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.ProvisioningResponse.OtaKeybox) + }) + , + 'DESCRIPTOR' : _PROVISIONINGRESPONSE, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.ProvisioningResponse) + }) +_sym_db.RegisterMessage(ProvisioningResponse) +_sym_db.RegisterMessage(ProvisioningResponse.OtaKeybox) + +SignedProvisioningContext = _reflection.GeneratedProtocolMessageType('SignedProvisioningContext', (_message.Message,), { + 'DESCRIPTOR' : _SIGNEDPROVISIONINGCONTEXT, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.SignedProvisioningContext) + }) +_sym_db.RegisterMessage(SignedProvisioningContext) + +SignedProvisioningMessage = _reflection.GeneratedProtocolMessageType('SignedProvisioningMessage', (_message.Message,), { + 'DESCRIPTOR' : _SIGNEDPROVISIONINGMESSAGE, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.SignedProvisioningMessage) + }) +_sym_db.RegisterMessage(SignedProvisioningMessage) + +ClientIdentification = _reflection.GeneratedProtocolMessageType('ClientIdentification', (_message.Message,), { + + 'NameValue' : _reflection.GeneratedProtocolMessageType('NameValue', (_message.Message,), { + 'DESCRIPTOR' : _CLIENTIDENTIFICATION_NAMEVALUE, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.ClientIdentification.NameValue) + }) + , + + 'ClientCapabilities' : _reflection.GeneratedProtocolMessageType('ClientCapabilities', (_message.Message,), { + 'DESCRIPTOR' : _CLIENTIDENTIFICATION_CLIENTCAPABILITIES, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.ClientIdentification.ClientCapabilities) + }) + , + + 'ClientCredentials' : _reflection.GeneratedProtocolMessageType('ClientCredentials', (_message.Message,), { + 'DESCRIPTOR' : _CLIENTIDENTIFICATION_CLIENTCREDENTIALS, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.ClientIdentification.ClientCredentials) + }) + , + 'DESCRIPTOR' : _CLIENTIDENTIFICATION, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.ClientIdentification) + }) +_sym_db.RegisterMessage(ClientIdentification) +_sym_db.RegisterMessage(ClientIdentification.NameValue) +_sym_db.RegisterMessage(ClientIdentification.ClientCapabilities) +_sym_db.RegisterMessage(ClientIdentification.ClientCredentials) + +EncryptedClientIdentification = _reflection.GeneratedProtocolMessageType('EncryptedClientIdentification', (_message.Message,), { + 'DESCRIPTOR' : _ENCRYPTEDCLIENTIDENTIFICATION, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.EncryptedClientIdentification) + }) +_sym_db.RegisterMessage(EncryptedClientIdentification) + +DrmCertificate = _reflection.GeneratedProtocolMessageType('DrmCertificate', (_message.Message,), { + + 'EncryptionKey' : _reflection.GeneratedProtocolMessageType('EncryptionKey', (_message.Message,), { + 'DESCRIPTOR' : _DRMCERTIFICATE_ENCRYPTIONKEY, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.DrmCertificate.EncryptionKey) + }) + , + 'DESCRIPTOR' : _DRMCERTIFICATE, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.DrmCertificate) + }) +_sym_db.RegisterMessage(DrmCertificate) +_sym_db.RegisterMessage(DrmCertificate.EncryptionKey) + +SignedDrmCertificate = _reflection.GeneratedProtocolMessageType('SignedDrmCertificate', (_message.Message,), { + 'DESCRIPTOR' : _SIGNEDDRMCERTIFICATE, + '__module__' : 'license_pb2' + # @@protoc_insertion_point(class_scope:keydive_license_protocol.SignedDrmCertificate) + }) +_sym_db.RegisterMessage(SignedDrmCertificate) + +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\032com.keydive.license.protosH\003' + _DRMCERTIFICATE.fields_by_name['test_device_deprecated']._options = None + _DRMCERTIFICATE.fields_by_name['test_device_deprecated']._serialized_options = b'\030\001' + _LICENSETYPE._serialized_start=8127 + _LICENSETYPE._serialized_end=8183 + _PROTOCOLVERSION._serialized_start=8185 + _PROTOCOLVERSION._serialized_end=8253 + _HASHALGORITHMPROTO._serialized_start=8256 + _HASHALGORITHMPROTO._serialized_end=8390 + _LICENSEIDENTIFICATION._serialized_start=44 + _LICENSEIDENTIFICATION._serialized_end=353 + _LICENSEREQUEST._serialized_start=356 + _LICENSEREQUEST._serialized_end=2145 + _LICENSEREQUEST_CONTENTIDENTIFICATION._serialized_start=873 + _LICENSEREQUEST_CONTENTIDENTIFICATION._serialized_end=2013 + _LICENSEREQUEST_CONTENTIDENTIFICATION_WIDEVINEPSSHDATA._serialized_start=1308 + _LICENSEREQUEST_CONTENTIDENTIFICATION_WIDEVINEPSSHDATA._serialized_end=1426 + _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBMKEYID._serialized_start=1428 + _LICENSEREQUEST_CONTENTIDENTIFICATION_WEBMKEYID._serialized_end=1536 + _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_start=1539 + _LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE._serialized_end=1726 + _LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA._serialized_start=1729 + _LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA._serialized_end=1991 + _LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA_INITDATATYPE._serialized_start=1957 + _LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA_INITDATATYPE._serialized_end=1991 + _LICENSEREQUEST_SUBSESSIONDATA._serialized_start=2015 + _LICENSEREQUEST_SUBSESSIONDATA._serialized_end=2095 + _LICENSEREQUEST_REQUESTTYPE._serialized_start=2097 + _LICENSEREQUEST_REQUESTTYPE._serialized_end=2145 + _METRICDATA._serialized_start=2148 + _METRICDATA._serialized_end=2385 + _METRICDATA_TYPEVALUE._serialized_start=2251 + _METRICDATA_TYPEVALUE._serialized_end=2343 + _METRICDATA_METRICTYPE._serialized_start=2345 + _METRICDATA_METRICTYPE._serialized_end=2385 + _VERSIONINFO._serialized_start=2387 + _VERSIONINFO._serialized_end=2462 + _SIGNEDMESSAGE._serialized_start=2465 + _SIGNEDMESSAGE._serialized_end=3247 + _SIGNEDMESSAGE_MESSAGETYPE._serialized_start=2926 + _SIGNEDMESSAGE_MESSAGETYPE._serialized_end=3162 + _SIGNEDMESSAGE_SESSIONKEYTYPE._serialized_start=3164 + _SIGNEDMESSAGE_SESSIONKEYTYPE._serialized_end=3247 + _PROVISIONINGRESPONSE._serialized_start=3250 + _PROVISIONINGRESPONSE._serialized_end=3746 + _PROVISIONINGRESPONSE_OTAKEYBOX._serialized_start=3551 + _PROVISIONINGRESPONSE_OTAKEYBOX._serialized_end=3651 + _PROVISIONINGRESPONSE_PROVISIONINGSTATUS._serialized_start=3653 + _PROVISIONINGRESPONSE_PROVISIONINGSTATUS._serialized_end=3746 + _SIGNEDPROVISIONINGCONTEXT._serialized_start=3748 + _SIGNEDPROVISIONINGCONTEXT._serialized_end=3824 + _SIGNEDPROVISIONINGMESSAGE._serialized_start=3827 + _SIGNEDPROVISIONINGMESSAGE._serialized_end=4610 + _SIGNEDPROVISIONINGMESSAGE_PROVISIONINGPROTOCOLVERSION._serialized_start=4333 + _SIGNEDPROVISIONINGMESSAGE_PROVISIONINGPROTOCOLVERSION._serialized_end=4434 + _SIGNEDPROVISIONINGMESSAGE_PROVISIONINGTYPE._serialized_start=4437 + _SIGNEDPROVISIONINGMESSAGE_PROVISIONINGTYPE._serialized_end=4610 + _CLIENTIDENTIFICATION._serialized_start=4613 + _CLIENTIDENTIFICATION._serialized_end=6790 + _CLIENTIDENTIFICATION_NAMEVALUE._serialized_start=5075 + _CLIENTIDENTIFICATION_NAMEVALUE._serialized_end=5115 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES._serialized_start=5118 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES._serialized_end=6557 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION._serialized_start=6029 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION._serialized_end=6157 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE._serialized_start=6159 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE._serialized_end=6264 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_ANALOGOUTPUTCAPABILITIES._serialized_start=6267 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_ANALOGOUTPUTCAPABILITIES._serialized_end=6408 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_WATERMARKINGSUPPORT._serialized_start=6411 + _CLIENTIDENTIFICATION_CLIENTCAPABILITIES_WATERMARKINGSUPPORT._serialized_end=6557 + _CLIENTIDENTIFICATION_CLIENTCREDENTIALS._serialized_start=6559 + _CLIENTIDENTIFICATION_CLIENTCREDENTIALS._serialized_end=6673 + _CLIENTIDENTIFICATION_TOKENTYPE._serialized_start=6675 + _CLIENTIDENTIFICATION_TOKENTYPE._serialized_end=6790 + _ENCRYPTEDCLIENTIDENTIFICATION._serialized_start=6793 + _ENCRYPTEDCLIENTIDENTIFICATION._serialized_end=6980 + _DRMCERTIFICATE._serialized_start=6983 + _DRMCERTIFICATE._serialized_end=7922 + _DRMCERTIFICATE_ENCRYPTIONKEY._serialized_start=7494 + _DRMCERTIFICATE_ENCRYPTIONKEY._serialized_end=7605 + _DRMCERTIFICATE_TYPE._serialized_start=7607 + _DRMCERTIFICATE_TYPE._serialized_end=7683 + _DRMCERTIFICATE_SERVICETYPE._serialized_start=7686 + _DRMCERTIFICATE_SERVICETYPE._serialized_end=7820 + _DRMCERTIFICATE_ALGORITHM._serialized_start=7822 + _DRMCERTIFICATE_ALGORITHM._serialized_end=7922 + _SIGNEDDRMCERTIFICATE._serialized_start=7925 + _SIGNEDDRMCERTIFICATE._serialized_end=8125 +# @@protoc_insertion_point(module_scope) diff --git a/keydive/drm/protocol/license_pb2.pyi b/keydive/drm/protocol/license_pb2.pyi new file mode 100644 index 0000000..288d4f7 --- /dev/null +++ b/keydive/drm/protocol/license_pb2.pyi @@ -0,0 +1,1391 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _LicenseType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _LicenseTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_LicenseType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + STREAMING: _LicenseType.ValueType # 1 + OFFLINE: _LicenseType.ValueType # 2 + AUTOMATIC: _LicenseType.ValueType # 3 + """License type decision is left to provider.""" + +class LicenseType(_LicenseType, metaclass=_LicenseTypeEnumTypeWrapper): + """---------------------------------------------------------------------------- + license_protocol.proto + ---------------------------------------------------------------------------- + Description of section: + Definitions of the protocol buffer messages used in the Widevine license + exchange protocol, described in Widevine license exchange protocol document + """ + +STREAMING: LicenseType.ValueType # 1 +OFFLINE: LicenseType.ValueType # 2 +AUTOMATIC: LicenseType.ValueType # 3 +"""License type decision is left to provider.""" +global___LicenseType = LicenseType + +class _ProtocolVersion: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _ProtocolVersionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ProtocolVersion.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + VERSION_2_0: _ProtocolVersion.ValueType # 20 + VERSION_2_1: _ProtocolVersion.ValueType # 21 + VERSION_2_2: _ProtocolVersion.ValueType # 22 + +class ProtocolVersion(_ProtocolVersion, metaclass=_ProtocolVersionEnumTypeWrapper): ... + +VERSION_2_0: ProtocolVersion.ValueType # 20 +VERSION_2_1: ProtocolVersion.ValueType # 21 +VERSION_2_2: ProtocolVersion.ValueType # 22 +global___ProtocolVersion = ProtocolVersion + +class _HashAlgorithmProto: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _HashAlgorithmProtoEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_HashAlgorithmProto.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + HASH_ALGORITHM_UNSPECIFIED: _HashAlgorithmProto.ValueType # 0 + """Unspecified hash algorithm: SHA_256 shall be used for ECC based algorithms + and SHA_1 shall be used otherwise. + """ + HASH_ALGORITHM_SHA_1: _HashAlgorithmProto.ValueType # 1 + HASH_ALGORITHM_SHA_256: _HashAlgorithmProto.ValueType # 2 + HASH_ALGORITHM_SHA_384: _HashAlgorithmProto.ValueType # 3 + +class HashAlgorithmProto(_HashAlgorithmProto, metaclass=_HashAlgorithmProtoEnumTypeWrapper): + """---------------------------------------------------------------------------- + hash_algorithm.proto + ---------------------------------------------------------------------------- + Description of section: + Public protocol buffer definitions for Widevine Hash Algorithm protocol. + """ + +HASH_ALGORITHM_UNSPECIFIED: HashAlgorithmProto.ValueType # 0 +"""Unspecified hash algorithm: SHA_256 shall be used for ECC based algorithms +and SHA_1 shall be used otherwise. +""" +HASH_ALGORITHM_SHA_1: HashAlgorithmProto.ValueType # 1 +HASH_ALGORITHM_SHA_256: HashAlgorithmProto.ValueType # 2 +HASH_ALGORITHM_SHA_384: HashAlgorithmProto.ValueType # 3 +global___HashAlgorithmProto = HashAlgorithmProto + +@typing.final +class LicenseIdentification(google.protobuf.message.Message): + """LicenseIdentification is propagated from LicenseRequest to License, + incrementing version with each iteration. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + REQUEST_ID_FIELD_NUMBER: builtins.int + SESSION_ID_FIELD_NUMBER: builtins.int + PURCHASE_ID_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + PROVIDER_SESSION_TOKEN_FIELD_NUMBER: builtins.int + ORIGINAL_RENTAL_DURATION_SECONDS_FIELD_NUMBER: builtins.int + ORIGINAL_PLAYBACK_DURATION_SECONDS_FIELD_NUMBER: builtins.int + ORIGINAL_START_TIME_SECONDS_FIELD_NUMBER: builtins.int + request_id: builtins.bytes + session_id: builtins.bytes + purchase_id: builtins.bytes + type: global___LicenseType.ValueType + version: builtins.int + provider_session_token: builtins.bytes + original_rental_duration_seconds: builtins.int + original_playback_duration_seconds: builtins.int + original_start_time_seconds: builtins.int + def __init__( + self, + *, + request_id: builtins.bytes | None = ..., + session_id: builtins.bytes | None = ..., + purchase_id: builtins.bytes | None = ..., + type: global___LicenseType.ValueType | None = ..., + version: builtins.int | None = ..., + provider_session_token: builtins.bytes | None = ..., + original_rental_duration_seconds: builtins.int | None = ..., + original_playback_duration_seconds: builtins.int | None = ..., + original_start_time_seconds: builtins.int | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["original_playback_duration_seconds", b"original_playback_duration_seconds", "original_rental_duration_seconds", b"original_rental_duration_seconds", "original_start_time_seconds", b"original_start_time_seconds", "provider_session_token", b"provider_session_token", "purchase_id", b"purchase_id", "request_id", b"request_id", "session_id", b"session_id", "type", b"type", "version", b"version"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["original_playback_duration_seconds", b"original_playback_duration_seconds", "original_rental_duration_seconds", b"original_rental_duration_seconds", "original_start_time_seconds", b"original_start_time_seconds", "provider_session_token", b"provider_session_token", "purchase_id", b"purchase_id", "request_id", b"request_id", "session_id", b"session_id", "type", b"type", "version", b"version"]) -> None: ... + +global___LicenseIdentification = LicenseIdentification + +@typing.final +class LicenseRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _RequestType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _RequestTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[LicenseRequest._RequestType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + NEW: LicenseRequest._RequestType.ValueType # 1 + RENEWAL: LicenseRequest._RequestType.ValueType # 2 + RELEASE: LicenseRequest._RequestType.ValueType # 3 + + class RequestType(_RequestType, metaclass=_RequestTypeEnumTypeWrapper): ... + NEW: LicenseRequest.RequestType.ValueType # 1 + RENEWAL: LicenseRequest.RequestType.ValueType # 2 + RELEASE: LicenseRequest.RequestType.ValueType # 3 + + @typing.final + class ContentIdentification(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + @typing.final + class WidevinePsshData(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PSSH_DATA_FIELD_NUMBER: builtins.int + LICENSE_TYPE_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + license_type: global___LicenseType.ValueType + request_id: builtins.bytes + """Opaque, client-specified.""" + @property + def pssh_data(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bytes]: + """repeated WidevinePsshData pssh_data = 1;""" + + def __init__( + self, + *, + pssh_data: collections.abc.Iterable[builtins.bytes] | None = ..., + license_type: global___LicenseType.ValueType | None = ..., + request_id: builtins.bytes | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["license_type", b"license_type", "request_id", b"request_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["license_type", b"license_type", "pssh_data", b"pssh_data", "request_id", b"request_id"]) -> None: ... + + @typing.final + class WebmKeyId(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + HEADER_FIELD_NUMBER: builtins.int + LICENSE_TYPE_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + header: builtins.bytes + license_type: global___LicenseType.ValueType + request_id: builtins.bytes + """Opaque, client-specified.""" + def __init__( + self, + *, + header: builtins.bytes | None = ..., + license_type: global___LicenseType.ValueType | None = ..., + request_id: builtins.bytes | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["header", b"header", "license_type", b"license_type", "request_id", b"request_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["header", b"header", "license_type", b"license_type", "request_id", b"request_id"]) -> None: ... + + @typing.final + class ExistingLicense(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + LICENSE_ID_FIELD_NUMBER: builtins.int + SECONDS_SINCE_STARTED_FIELD_NUMBER: builtins.int + SECONDS_SINCE_LAST_PLAYED_FIELD_NUMBER: builtins.int + SESSION_USAGE_TABLE_ENTRY_FIELD_NUMBER: builtins.int + seconds_since_started: builtins.int + seconds_since_last_played: builtins.int + session_usage_table_entry: builtins.bytes + @property + def license_id(self) -> global___LicenseIdentification: ... + def __init__( + self, + *, + license_id: global___LicenseIdentification | None = ..., + seconds_since_started: builtins.int | None = ..., + seconds_since_last_played: builtins.int | None = ..., + session_usage_table_entry: builtins.bytes | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["license_id", b"license_id", "seconds_since_last_played", b"seconds_since_last_played", "seconds_since_started", b"seconds_since_started", "session_usage_table_entry", b"session_usage_table_entry"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["license_id", b"license_id", "seconds_since_last_played", b"seconds_since_last_played", "seconds_since_started", b"seconds_since_started", "session_usage_table_entry", b"session_usage_table_entry"]) -> None: ... + + @typing.final + class InitData(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _InitDataType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _InitDataTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[LicenseRequest.ContentIdentification.InitData._InitDataType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + CENC: LicenseRequest.ContentIdentification.InitData._InitDataType.ValueType # 1 + WEBM: LicenseRequest.ContentIdentification.InitData._InitDataType.ValueType # 2 + + class InitDataType(_InitDataType, metaclass=_InitDataTypeEnumTypeWrapper): ... + CENC: LicenseRequest.ContentIdentification.InitData.InitDataType.ValueType # 1 + WEBM: LicenseRequest.ContentIdentification.InitData.InitDataType.ValueType # 2 + + INIT_DATA_TYPE_FIELD_NUMBER: builtins.int + INIT_DATA_FIELD_NUMBER: builtins.int + LICENSE_TYPE_FIELD_NUMBER: builtins.int + REQUEST_ID_FIELD_NUMBER: builtins.int + init_data_type: global___LicenseRequest.ContentIdentification.InitData.InitDataType.ValueType + init_data: builtins.bytes + license_type: global___LicenseType.ValueType + request_id: builtins.bytes + def __init__( + self, + *, + init_data_type: global___LicenseRequest.ContentIdentification.InitData.InitDataType.ValueType | None = ..., + init_data: builtins.bytes | None = ..., + license_type: global___LicenseType.ValueType | None = ..., + request_id: builtins.bytes | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["init_data", b"init_data", "init_data_type", b"init_data_type", "license_type", b"license_type", "request_id", b"request_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["init_data", b"init_data", "init_data_type", b"init_data_type", "license_type", b"license_type", "request_id", b"request_id"]) -> None: ... + + WIDEVINE_PSSH_DATA_FIELD_NUMBER: builtins.int + WEBM_KEY_ID_FIELD_NUMBER: builtins.int + EXISTING_LICENSE_FIELD_NUMBER: builtins.int + INIT_DATA_FIELD_NUMBER: builtins.int + @property + def widevine_pssh_data(self) -> global___LicenseRequest.ContentIdentification.WidevinePsshData: + """Exactly one of these must be present.""" + + @property + def webm_key_id(self) -> global___LicenseRequest.ContentIdentification.WebmKeyId: ... + @property + def existing_license(self) -> global___LicenseRequest.ContentIdentification.ExistingLicense: ... + @property + def init_data(self) -> global___LicenseRequest.ContentIdentification.InitData: ... + def __init__( + self, + *, + widevine_pssh_data: global___LicenseRequest.ContentIdentification.WidevinePsshData | None = ..., + webm_key_id: global___LicenseRequest.ContentIdentification.WebmKeyId | None = ..., + existing_license: global___LicenseRequest.ContentIdentification.ExistingLicense | None = ..., + init_data: global___LicenseRequest.ContentIdentification.InitData | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["content_id_variant", b"content_id_variant", "existing_license", b"existing_license", "init_data", b"init_data", "webm_key_id", b"webm_key_id", "widevine_pssh_data", b"widevine_pssh_data"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["content_id_variant", b"content_id_variant", "existing_license", b"existing_license", "init_data", b"init_data", "webm_key_id", b"webm_key_id", "widevine_pssh_data", b"widevine_pssh_data"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["content_id_variant", b"content_id_variant"]) -> typing.Literal["widevine_pssh_data", "webm_key_id", "existing_license", "init_data"] | None: ... + + @typing.final + class SubSessionData(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SUB_SESSION_KEY_ID_FIELD_NUMBER: builtins.int + NONCE_FIELD_NUMBER: builtins.int + TRACK_LABEL_FIELD_NUMBER: builtins.int + sub_session_key_id: builtins.str + """Required. The key ID for the corresponding SUB_SESSION_KEY. The + value must match the sub_session_key_id field for a + corresponding SubLicense message from the PSSH. + """ + nonce: builtins.int + """Required. The nonce for the track.""" + track_label: builtins.str + """Required for initial license request used for each CONTENT key_container + to know which nonce to use for building its key control block. + Not needed for renewal license request. + """ + def __init__( + self, + *, + sub_session_key_id: builtins.str | None = ..., + nonce: builtins.int | None = ..., + track_label: builtins.str | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["nonce", b"nonce", "sub_session_key_id", b"sub_session_key_id", "track_label", b"track_label"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["nonce", b"nonce", "sub_session_key_id", b"sub_session_key_id", "track_label", b"track_label"]) -> None: ... + + CLIENT_ID_FIELD_NUMBER: builtins.int + CONTENT_ID_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + REQUEST_TIME_FIELD_NUMBER: builtins.int + KEY_CONTROL_NONCE_DEPRECATED_FIELD_NUMBER: builtins.int + PROTOCOL_VERSION_FIELD_NUMBER: builtins.int + KEY_CONTROL_NONCE_FIELD_NUMBER: builtins.int + ENCRYPTED_CLIENT_ID_FIELD_NUMBER: builtins.int + CLIENT_VERSION_FIELD_NUMBER: builtins.int + type: global___LicenseRequest.RequestType.ValueType + request_time: builtins.int + """Time of the request in seconds (UTC) as set by the client.""" + key_control_nonce_deprecated: builtins.bytes + """Old-style decimal-encoded string key control nonce.""" + protocol_version: global___ProtocolVersion.ValueType + key_control_nonce: builtins.int + """New-style uint32 key control nonce, please use instead of + key_control_nonce_deprecated. + """ + client_version: builtins.str + """Optional sub session context information. Required for using + SubLicenses from the PSSH. + repeated SubSessionData sub_session_data = 9; + """ + @property + def client_id(self) -> global___ClientIdentification: + """The client_id provides information authenticating the calling device. It + contains the Widevine keybox token that was installed on the device at the + factory. This field or encrypted_client_id below is required for a valid + license request, but both should never be present in the same request. + """ + + @property + def content_id(self) -> global___LicenseRequest.ContentIdentification: ... + @property + def encrypted_client_id(self) -> global___EncryptedClientIdentification: + """Encrypted ClientIdentification message, used for privacy purposes.""" + + def __init__( + self, + *, + client_id: global___ClientIdentification | None = ..., + content_id: global___LicenseRequest.ContentIdentification | None = ..., + type: global___LicenseRequest.RequestType.ValueType | None = ..., + request_time: builtins.int | None = ..., + key_control_nonce_deprecated: builtins.bytes | None = ..., + protocol_version: global___ProtocolVersion.ValueType | None = ..., + key_control_nonce: builtins.int | None = ..., + encrypted_client_id: global___EncryptedClientIdentification | None = ..., + client_version: builtins.str | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["client_id", b"client_id", "client_version", b"client_version", "content_id", b"content_id", "encrypted_client_id", b"encrypted_client_id", "key_control_nonce", b"key_control_nonce", "key_control_nonce_deprecated", b"key_control_nonce_deprecated", "protocol_version", b"protocol_version", "request_time", b"request_time", "type", b"type"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["client_id", b"client_id", "client_version", b"client_version", "content_id", b"content_id", "encrypted_client_id", b"encrypted_client_id", "key_control_nonce", b"key_control_nonce", "key_control_nonce_deprecated", b"key_control_nonce_deprecated", "protocol_version", b"protocol_version", "request_time", b"request_time", "type", b"type"]) -> None: ... + +global___LicenseRequest = LicenseRequest + +@typing.final +class MetricData(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _MetricType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _MetricTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[MetricData._MetricType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + LATENCY: MetricData._MetricType.ValueType # 1 + """The time spent in the 'stage', specified in microseconds.""" + TIMESTAMP: MetricData._MetricType.ValueType # 2 + """The UNIX epoch timestamp at which the 'stage' was first accessed in + microseconds. + """ + + class MetricType(_MetricType, metaclass=_MetricTypeEnumTypeWrapper): ... + LATENCY: MetricData.MetricType.ValueType # 1 + """The time spent in the 'stage', specified in microseconds.""" + TIMESTAMP: MetricData.MetricType.ValueType # 2 + """The UNIX epoch timestamp at which the 'stage' was first accessed in + microseconds. + """ + + @typing.final + class TypeValue(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + type: global___MetricData.MetricType.ValueType + value: builtins.int + """The value associated with 'type'. For example if type == LATENCY, the + value would be the time in microseconds spent in this 'stage'. + """ + def __init__( + self, + *, + type: global___MetricData.MetricType.ValueType | None = ..., + value: builtins.int | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["type", b"type", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["type", b"type", "value", b"value"]) -> None: ... + + STAGE_NAME_FIELD_NUMBER: builtins.int + METRIC_DATA_FIELD_NUMBER: builtins.int + stage_name: builtins.str + """'stage' that is currently processing the SignedMessage. Required.""" + @property + def metric_data(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricData.TypeValue]: + """metric and associated value.""" + + def __init__( + self, + *, + stage_name: builtins.str | None = ..., + metric_data: collections.abc.Iterable[global___MetricData.TypeValue] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["stage_name", b"stage_name"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["metric_data", b"metric_data", "stage_name", b"stage_name"]) -> None: ... + +global___MetricData = MetricData + +@typing.final +class VersionInfo(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + LICENSE_SDK_VERSION_FIELD_NUMBER: builtins.int + LICENSE_SERVICE_VERSION_FIELD_NUMBER: builtins.int + license_sdk_version: builtins.str + """License SDK version reported by the Widevine License SDK. This field + is populated automatically by the SDK. + """ + license_service_version: builtins.str + """Version of the service hosting the license SDK. This field is optional. + It may be provided by the hosting service. + """ + def __init__( + self, + *, + license_sdk_version: builtins.str | None = ..., + license_service_version: builtins.str | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["license_sdk_version", b"license_sdk_version", "license_service_version", b"license_service_version"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["license_sdk_version", b"license_sdk_version", "license_service_version", b"license_service_version"]) -> None: ... + +global___VersionInfo = VersionInfo + +@typing.final +class SignedMessage(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _MessageType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _MessageTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SignedMessage._MessageType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + LICENSE_REQUEST: SignedMessage._MessageType.ValueType # 1 + LICENSE: SignedMessage._MessageType.ValueType # 2 + ERROR_RESPONSE: SignedMessage._MessageType.ValueType # 3 + SERVICE_CERTIFICATE_REQUEST: SignedMessage._MessageType.ValueType # 4 + SERVICE_CERTIFICATE: SignedMessage._MessageType.ValueType # 5 + SUB_LICENSE: SignedMessage._MessageType.ValueType # 6 + CAS_LICENSE_REQUEST: SignedMessage._MessageType.ValueType # 7 + CAS_LICENSE: SignedMessage._MessageType.ValueType # 8 + EXTERNAL_LICENSE_REQUEST: SignedMessage._MessageType.ValueType # 9 + EXTERNAL_LICENSE: SignedMessage._MessageType.ValueType # 10 + + class MessageType(_MessageType, metaclass=_MessageTypeEnumTypeWrapper): ... + LICENSE_REQUEST: SignedMessage.MessageType.ValueType # 1 + LICENSE: SignedMessage.MessageType.ValueType # 2 + ERROR_RESPONSE: SignedMessage.MessageType.ValueType # 3 + SERVICE_CERTIFICATE_REQUEST: SignedMessage.MessageType.ValueType # 4 + SERVICE_CERTIFICATE: SignedMessage.MessageType.ValueType # 5 + SUB_LICENSE: SignedMessage.MessageType.ValueType # 6 + CAS_LICENSE_REQUEST: SignedMessage.MessageType.ValueType # 7 + CAS_LICENSE: SignedMessage.MessageType.ValueType # 8 + EXTERNAL_LICENSE_REQUEST: SignedMessage.MessageType.ValueType # 9 + EXTERNAL_LICENSE: SignedMessage.MessageType.ValueType # 10 + + class _SessionKeyType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _SessionKeyTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SignedMessage._SessionKeyType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNDEFINED: SignedMessage._SessionKeyType.ValueType # 0 + WRAPPED_AES_KEY: SignedMessage._SessionKeyType.ValueType # 1 + EPHERMERAL_ECC_PUBLIC_KEY: SignedMessage._SessionKeyType.ValueType # 2 + + class SessionKeyType(_SessionKeyType, metaclass=_SessionKeyTypeEnumTypeWrapper): ... + UNDEFINED: SignedMessage.SessionKeyType.ValueType # 0 + WRAPPED_AES_KEY: SignedMessage.SessionKeyType.ValueType # 1 + EPHERMERAL_ECC_PUBLIC_KEY: SignedMessage.SessionKeyType.ValueType # 2 + + TYPE_FIELD_NUMBER: builtins.int + MSG_FIELD_NUMBER: builtins.int + SIGNATURE_FIELD_NUMBER: builtins.int + SESSION_KEY_FIELD_NUMBER: builtins.int + REMOTE_ATTESTATION_FIELD_NUMBER: builtins.int + METRIC_DATA_FIELD_NUMBER: builtins.int + SERVICE_VERSION_INFO_FIELD_NUMBER: builtins.int + SESSION_KEY_TYPE_FIELD_NUMBER: builtins.int + OEMCRYPTO_CORE_MESSAGE_FIELD_NUMBER: builtins.int + USING_SECONDARY_KEY_FIELD_NUMBER: builtins.int + type: global___SignedMessage.MessageType.ValueType + msg: builtins.bytes + signature: builtins.bytes + """Required field that contains the signature of the bytes of msg. + For license requests, the signing algorithm is determined by the + certificate contained in the request. + For license responses, the signing algorithm is HMAC with signing key based + on |session_key|. + """ + session_key: builtins.bytes + """If populated, the contents of this field will be signaled by the + |session_key_type| type. If the |session_key_type| is WRAPPED_AES_KEY the + key is the bytes of an encrypted AES key. If the |session_key_type| is + EPHERMERAL_ECC_PUBLIC_KEY the field contains the bytes of an RFC5208 ASN1 + serialized ECC public key. + """ + remote_attestation: builtins.bytes + """Remote attestation data which will be present in the initial license + request for ChromeOS client devices operating in verified mode. Remote + attestation challenge data is |msg| field above. Optional. + """ + session_key_type: global___SignedMessage.SessionKeyType.ValueType + """Optional field that contains the algorithm type used to generate the + session_key and signature in a LICENSE message. + """ + oemcrypto_core_message: builtins.bytes + """The core message is the simple serialization of fields used by OEMCrypto. + This field was introduced in OEMCrypto API v16. + """ + using_secondary_key: builtins.bool + @property + def metric_data(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MetricData]: ... + @property + def service_version_info(self) -> global___VersionInfo: + """Version information from the SDK and license service. This information is + provided in the license response. + """ + + def __init__( + self, + *, + type: global___SignedMessage.MessageType.ValueType | None = ..., + msg: builtins.bytes | None = ..., + signature: builtins.bytes | None = ..., + session_key: builtins.bytes | None = ..., + remote_attestation: builtins.bytes | None = ..., + metric_data: collections.abc.Iterable[global___MetricData] | None = ..., + service_version_info: global___VersionInfo | None = ..., + session_key_type: global___SignedMessage.SessionKeyType.ValueType | None = ..., + oemcrypto_core_message: builtins.bytes | None = ..., + using_secondary_key: builtins.bool | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["msg", b"msg", "oemcrypto_core_message", b"oemcrypto_core_message", "remote_attestation", b"remote_attestation", "service_version_info", b"service_version_info", "session_key", b"session_key", "session_key_type", b"session_key_type", "signature", b"signature", "type", b"type", "using_secondary_key", b"using_secondary_key"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["metric_data", b"metric_data", "msg", b"msg", "oemcrypto_core_message", b"oemcrypto_core_message", "remote_attestation", b"remote_attestation", "service_version_info", b"service_version_info", "session_key", b"session_key", "session_key_type", b"session_key_type", "signature", b"signature", "type", b"type", "using_secondary_key", b"using_secondary_key"]) -> None: ... + +global___SignedMessage = SignedMessage + +@typing.final +class ProvisioningResponse(google.protobuf.message.Message): + """---------------------------------------------------------------------------- + certificate_provisioning.proto + ---------------------------------------------------------------------------- + Description of section: + Public protocol buffer definitions for Widevine Device Certificate + Provisioning protocol. + + Provisioning response sent by the provisioning server to client devices. + This message is used for both regular Widevine DRM certificates and for + application-specific X.509 certificates. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _ProvisioningStatus: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _ProvisioningStatusEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ProvisioningResponse._ProvisioningStatus.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + NO_ERROR: ProvisioningResponse._ProvisioningStatus.ValueType # 0 + """Indicates a valid provisioning response""" + REVOKED_DEVICE_CREDENTIALS: ProvisioningResponse._ProvisioningStatus.ValueType # 1 + """The device credentials have been revoked. Provisioning is not possible.""" + REVOKED_DEVICE_SERIES: ProvisioningResponse._ProvisioningStatus.ValueType # 2 + """Devices in this series have been revoked. Provisioning is not possible.""" + + class ProvisioningStatus(_ProvisioningStatus, metaclass=_ProvisioningStatusEnumTypeWrapper): ... + NO_ERROR: ProvisioningResponse.ProvisioningStatus.ValueType # 0 + """Indicates a valid provisioning response""" + REVOKED_DEVICE_CREDENTIALS: ProvisioningResponse.ProvisioningStatus.ValueType # 1 + """The device credentials have been revoked. Provisioning is not possible.""" + REVOKED_DEVICE_SERIES: ProvisioningResponse.ProvisioningStatus.ValueType # 2 + """Devices in this series have been revoked. Provisioning is not possible.""" + + @typing.final + class OtaKeybox(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DEVICE_KEY_ENCRYPTION_IV_FIELD_NUMBER: builtins.int + ENCRYPTED_DEVICE_KEY_FIELD_NUMBER: builtins.int + DEVICE_CA_TOKEN_FIELD_NUMBER: builtins.int + device_key_encryption_iv: builtins.bytes + """Iv used along with SessionKeys.encryption_key for encrypting device key.""" + encrypted_device_key: builtins.bytes + """Device key component of the keybox, encrypted using the + SessionKeys.encryption_key in the request and |device_key_encryption_iv| + above. + """ + device_ca_token: builtins.bytes + """Device CA token component of the keybox.""" + def __init__( + self, + *, + device_key_encryption_iv: builtins.bytes | None = ..., + encrypted_device_key: builtins.bytes | None = ..., + device_ca_token: builtins.bytes | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["device_ca_token", b"device_ca_token", "device_key_encryption_iv", b"device_key_encryption_iv", "encrypted_device_key", b"encrypted_device_key"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["device_ca_token", b"device_ca_token", "device_key_encryption_iv", b"device_key_encryption_iv", "encrypted_device_key", b"encrypted_device_key"]) -> None: ... + + DEVICE_RSA_KEY_FIELD_NUMBER: builtins.int + DEVICE_RSA_KEY_IV_FIELD_NUMBER: builtins.int + DEVICE_CERTIFICATE_FIELD_NUMBER: builtins.int + NONCE_FIELD_NUMBER: builtins.int + WRAPPING_KEY_FIELD_NUMBER: builtins.int + OTA_KEYBOX_FIELD_NUMBER: builtins.int + STATUS_FIELD_NUMBER: builtins.int + device_rsa_key: builtins.bytes + """AES-128 encrypted device private RSA key. PKCS#1 ASN.1 DER-encoded. + Required. For X.509 certificates, the private RSA key may also include + a prefix as specified by private_key_prefix in the X509CertificateMetadata + proto message. + """ + device_rsa_key_iv: builtins.bytes + """Initialization vector used to encrypt device_rsa_key. Required.""" + device_certificate: builtins.bytes + """For Widevine DRM certificates, this contains the serialized + SignedDrmCertificate. For X.509 certificates, this contains the PEM + encoded X.509 certificate. Required. + """ + nonce: builtins.bytes + """Nonce value matching nonce in ProvisioningRequest. Required.""" + wrapping_key: builtins.bytes + """Key used to wrap device_rsa_key when DRM provisioning an OEM factory + provisioned device. Encrypted with the device OEM public key using + RSA-OAEP. + """ + status: global___ProvisioningResponse.ProvisioningStatus.ValueType + """The provisioning service may return a ProvisioningStatus. Fields other + than |status| may be empty and should be ignored if the |status| + is present and not NO_ERROR + """ + @property + def ota_keybox(self) -> global___ProvisioningResponse.OtaKeybox: + """Only populated in OTA keybox provisioning response.""" + + def __init__( + self, + *, + device_rsa_key: builtins.bytes | None = ..., + device_rsa_key_iv: builtins.bytes | None = ..., + device_certificate: builtins.bytes | None = ..., + nonce: builtins.bytes | None = ..., + wrapping_key: builtins.bytes | None = ..., + ota_keybox: global___ProvisioningResponse.OtaKeybox | None = ..., + status: global___ProvisioningResponse.ProvisioningStatus.ValueType | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["device_certificate", b"device_certificate", "device_rsa_key", b"device_rsa_key", "device_rsa_key_iv", b"device_rsa_key_iv", "nonce", b"nonce", "ota_keybox", b"ota_keybox", "status", b"status", "wrapping_key", b"wrapping_key"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["device_certificate", b"device_certificate", "device_rsa_key", b"device_rsa_key", "device_rsa_key_iv", b"device_rsa_key_iv", "nonce", b"nonce", "ota_keybox", b"ota_keybox", "status", b"status", "wrapping_key", b"wrapping_key"]) -> None: ... + +global___ProvisioningResponse = ProvisioningResponse + +@typing.final +class SignedProvisioningContext(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROVISIONING_CONTEXT_FIELD_NUMBER: builtins.int + SIGNATURE_FIELD_NUMBER: builtins.int + provisioning_context: builtins.bytes + """ProvisioningContext in bytes.""" + signature: builtins.bytes + """RSASSA-PSS signature of provisioning_context. Signed with service private + key. + """ + def __init__( + self, + *, + provisioning_context: builtins.bytes | None = ..., + signature: builtins.bytes | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["provisioning_context", b"provisioning_context", "signature", b"signature"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["provisioning_context", b"provisioning_context", "signature", b"signature"]) -> None: ... + +global___SignedProvisioningContext = SignedProvisioningContext + +@typing.final +class SignedProvisioningMessage(google.protobuf.message.Message): + """Serialized ProvisioningRequest or ProvisioningResponse signed with + The message authentication key. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _ProvisioningProtocolVersion: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _ProvisioningProtocolVersionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SignedProvisioningMessage._ProvisioningProtocolVersion.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + VERSION_UNSPECIFIED: SignedProvisioningMessage._ProvisioningProtocolVersion.ValueType # 0 + VERSION_1: SignedProvisioningMessage._ProvisioningProtocolVersion.ValueType # 1 + VERSION_1_1: SignedProvisioningMessage._ProvisioningProtocolVersion.ValueType # 2 + """Version 1.1 changed error handling. Some errors are returned as a field + in a response message rather than being handled as errors via the API + implementation. E.g. embedded in the ProvisioningResponse rather than + returning a 400 error to the caller. + """ + VERSION_2: SignedProvisioningMessage._ProvisioningProtocolVersion.ValueType # 3 + """Version 2 will implement a larger change of the protocol definition + in protobufs. This will provide a cleaner separation between protocols. + """ + + class ProvisioningProtocolVersion(_ProvisioningProtocolVersion, metaclass=_ProvisioningProtocolVersionEnumTypeWrapper): ... + VERSION_UNSPECIFIED: SignedProvisioningMessage.ProvisioningProtocolVersion.ValueType # 0 + VERSION_1: SignedProvisioningMessage.ProvisioningProtocolVersion.ValueType # 1 + VERSION_1_1: SignedProvisioningMessage.ProvisioningProtocolVersion.ValueType # 2 + """Version 1.1 changed error handling. Some errors are returned as a field + in a response message rather than being handled as errors via the API + implementation. E.g. embedded in the ProvisioningResponse rather than + returning a 400 error to the caller. + """ + VERSION_2: SignedProvisioningMessage.ProvisioningProtocolVersion.ValueType # 3 + """Version 2 will implement a larger change of the protocol definition + in protobufs. This will provide a cleaner separation between protocols. + """ + + class _ProvisioningType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _ProvisioningTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[SignedProvisioningMessage._ProvisioningType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + PROVISIONING_TYPE_UNSPECIFIED: SignedProvisioningMessage._ProvisioningType.ValueType # 0 + SERVICE_CERTIFICATE_REQUEST: SignedProvisioningMessage._ProvisioningType.ValueType # 1 + """Service certificate request.""" + PROVISIONING_20: SignedProvisioningMessage._ProvisioningType.ValueType # 2 + """Keybox factory-provisioned devices.""" + PROVISIONING_30: SignedProvisioningMessage._ProvisioningType.ValueType # 3 + """OEM certificate factory-provisioned devices.""" + ARCPP_PROVISIONING: SignedProvisioningMessage._ProvisioningType.ValueType # 4 + """ChromeOS/Arc++ devices.""" + INTEL_SIGMA_101: SignedProvisioningMessage._ProvisioningType.ValueType # 101 + """Intel Sigma 1.0.1 protocol.""" + + class ProvisioningType(_ProvisioningType, metaclass=_ProvisioningTypeEnumTypeWrapper): + """This enum was renamed to avoid confusion""" + + PROVISIONING_TYPE_UNSPECIFIED: SignedProvisioningMessage.ProvisioningType.ValueType # 0 + SERVICE_CERTIFICATE_REQUEST: SignedProvisioningMessage.ProvisioningType.ValueType # 1 + """Service certificate request.""" + PROVISIONING_20: SignedProvisioningMessage.ProvisioningType.ValueType # 2 + """Keybox factory-provisioned devices.""" + PROVISIONING_30: SignedProvisioningMessage.ProvisioningType.ValueType # 3 + """OEM certificate factory-provisioned devices.""" + ARCPP_PROVISIONING: SignedProvisioningMessage.ProvisioningType.ValueType # 4 + """ChromeOS/Arc++ devices.""" + INTEL_SIGMA_101: SignedProvisioningMessage.ProvisioningType.ValueType # 101 + """Intel Sigma 1.0.1 protocol.""" + + MESSAGE_FIELD_NUMBER: builtins.int + SIGNATURE_FIELD_NUMBER: builtins.int + PROVISIONING_TYPE_FIELD_NUMBER: builtins.int + SIGNED_PROVISIONING_CONTEXT_FIELD_NUMBER: builtins.int + REMOTE_ATTESTATION_FIELD_NUMBER: builtins.int + OEMCRYPTO_CORE_MESSAGE_FIELD_NUMBER: builtins.int + HASH_ALGORITHM_FIELD_NUMBER: builtins.int + PROTOCOL_VERSION_FIELD_NUMBER: builtins.int + message: builtins.bytes + """Serialized protobuf message for the corresponding protocol and stage of + the provisioning exchange. ProvisioningRequest or ProvisioningResponse + in the case of Provisioning 2.0, 3.0 and ARCPP_PROVISIONING. Required. + """ + signature: builtins.bytes + """HMAC-SHA256 (Keybox) or RSASSA-PSS (OEM) signature of message. Required + for provisioning 2.0 and 3.0. For ARCPP_PROVISIONING, only used in + response. + """ + provisioning_type: global___SignedProvisioningMessage.ProvisioningType.ValueType + """Version number of provisioning protocol.""" + remote_attestation: builtins.bytes + """Remote attestation data to authenticate that the ChromeOS client device + is operating in verified mode. Remote attestation challenge data is + |message| field above. Required for ARCPP_PROVISIONING request. + It contains signature of |message|. + """ + oemcrypto_core_message: builtins.bytes + """The core message is the simple serialization of fields used by OEMCrypto. + This field was introduced in OEMCrypto API v16. The core message format is + documented in the "Widevine Core Message Serialization". + """ + hash_algorithm: global___HashAlgorithmProto.ValueType + """Optional field that indicates the hash algorithm used in signature scheme.""" + protocol_version: global___SignedProvisioningMessage.ProvisioningProtocolVersion.ValueType + """Indicates which version of the protocol is in use.""" + @property + def signed_provisioning_context(self) -> global___SignedProvisioningContext: + """Protocol-specific context / state information for multiple-exchange, + stateful provisioning protocols. Optional. + """ + + def __init__( + self, + *, + message: builtins.bytes | None = ..., + signature: builtins.bytes | None = ..., + provisioning_type: global___SignedProvisioningMessage.ProvisioningType.ValueType | None = ..., + signed_provisioning_context: global___SignedProvisioningContext | None = ..., + remote_attestation: builtins.bytes | None = ..., + oemcrypto_core_message: builtins.bytes | None = ..., + hash_algorithm: global___HashAlgorithmProto.ValueType | None = ..., + protocol_version: global___SignedProvisioningMessage.ProvisioningProtocolVersion.ValueType | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["hash_algorithm", b"hash_algorithm", "message", b"message", "oemcrypto_core_message", b"oemcrypto_core_message", "protocol_version", b"protocol_version", "provisioning_type", b"provisioning_type", "remote_attestation", b"remote_attestation", "signature", b"signature", "signed_provisioning_context", b"signed_provisioning_context"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["hash_algorithm", b"hash_algorithm", "message", b"message", "oemcrypto_core_message", b"oemcrypto_core_message", "protocol_version", b"protocol_version", "provisioning_type", b"provisioning_type", "remote_attestation", b"remote_attestation", "signature", b"signature", "signed_provisioning_context", b"signed_provisioning_context"]) -> None: ... + +global___SignedProvisioningMessage = SignedProvisioningMessage + +@typing.final +class ClientIdentification(google.protobuf.message.Message): + """---------------------------------------------------------------------------- + client_identification.proto + ---------------------------------------------------------------------------- + Description of section: + ClientIdentification messages used by provisioning and license protocols. + + ClientIdentification message used to authenticate the client device. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _TokenType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _TokenTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ClientIdentification._TokenType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + KEYBOX: ClientIdentification._TokenType.ValueType # 0 + DRM_DEVICE_CERTIFICATE: ClientIdentification._TokenType.ValueType # 1 + REMOTE_ATTESTATION_CERTIFICATE: ClientIdentification._TokenType.ValueType # 2 + OEM_DEVICE_CERTIFICATE: ClientIdentification._TokenType.ValueType # 3 + + class TokenType(_TokenType, metaclass=_TokenTypeEnumTypeWrapper): ... + KEYBOX: ClientIdentification.TokenType.ValueType # 0 + DRM_DEVICE_CERTIFICATE: ClientIdentification.TokenType.ValueType # 1 + REMOTE_ATTESTATION_CERTIFICATE: ClientIdentification.TokenType.ValueType # 2 + OEM_DEVICE_CERTIFICATE: ClientIdentification.TokenType.ValueType # 3 + + @typing.final + class NameValue(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + NAME_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + name: builtins.str + value: builtins.str + def __init__( + self, + *, + name: builtins.str | None = ..., + value: builtins.str | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["name", b"name", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["name", b"name", "value", b"value"]) -> None: ... + + @typing.final + class ClientCapabilities(google.protobuf.message.Message): + """Capabilities which not all clients may support. Used for the license + exchange protocol only. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _HdcpVersion: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _HdcpVersionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ClientIdentification.ClientCapabilities._HdcpVersion.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + HDCP_NONE: ClientIdentification.ClientCapabilities._HdcpVersion.ValueType # 0 + HDCP_V1: ClientIdentification.ClientCapabilities._HdcpVersion.ValueType # 1 + HDCP_V2: ClientIdentification.ClientCapabilities._HdcpVersion.ValueType # 2 + HDCP_V2_1: ClientIdentification.ClientCapabilities._HdcpVersion.ValueType # 3 + HDCP_V2_2: ClientIdentification.ClientCapabilities._HdcpVersion.ValueType # 4 + HDCP_V2_3: ClientIdentification.ClientCapabilities._HdcpVersion.ValueType # 5 + HDCP_NO_DIGITAL_OUTPUT: ClientIdentification.ClientCapabilities._HdcpVersion.ValueType # 255 + + class HdcpVersion(_HdcpVersion, metaclass=_HdcpVersionEnumTypeWrapper): ... + HDCP_NONE: ClientIdentification.ClientCapabilities.HdcpVersion.ValueType # 0 + HDCP_V1: ClientIdentification.ClientCapabilities.HdcpVersion.ValueType # 1 + HDCP_V2: ClientIdentification.ClientCapabilities.HdcpVersion.ValueType # 2 + HDCP_V2_1: ClientIdentification.ClientCapabilities.HdcpVersion.ValueType # 3 + HDCP_V2_2: ClientIdentification.ClientCapabilities.HdcpVersion.ValueType # 4 + HDCP_V2_3: ClientIdentification.ClientCapabilities.HdcpVersion.ValueType # 5 + HDCP_NO_DIGITAL_OUTPUT: ClientIdentification.ClientCapabilities.HdcpVersion.ValueType # 255 + + class _CertificateKeyType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _CertificateKeyTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ClientIdentification.ClientCapabilities._CertificateKeyType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + RSA_2048: ClientIdentification.ClientCapabilities._CertificateKeyType.ValueType # 0 + RSA_3072: ClientIdentification.ClientCapabilities._CertificateKeyType.ValueType # 1 + ECC_SECP256R1: ClientIdentification.ClientCapabilities._CertificateKeyType.ValueType # 2 + ECC_SECP384R1: ClientIdentification.ClientCapabilities._CertificateKeyType.ValueType # 3 + ECC_SECP521R1: ClientIdentification.ClientCapabilities._CertificateKeyType.ValueType # 4 + + class CertificateKeyType(_CertificateKeyType, metaclass=_CertificateKeyTypeEnumTypeWrapper): ... + RSA_2048: ClientIdentification.ClientCapabilities.CertificateKeyType.ValueType # 0 + RSA_3072: ClientIdentification.ClientCapabilities.CertificateKeyType.ValueType # 1 + ECC_SECP256R1: ClientIdentification.ClientCapabilities.CertificateKeyType.ValueType # 2 + ECC_SECP384R1: ClientIdentification.ClientCapabilities.CertificateKeyType.ValueType # 3 + ECC_SECP521R1: ClientIdentification.ClientCapabilities.CertificateKeyType.ValueType # 4 + + class _AnalogOutputCapabilities: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _AnalogOutputCapabilitiesEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ClientIdentification.ClientCapabilities._AnalogOutputCapabilities.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + ANALOG_OUTPUT_UNKNOWN: ClientIdentification.ClientCapabilities._AnalogOutputCapabilities.ValueType # 0 + ANALOG_OUTPUT_NONE: ClientIdentification.ClientCapabilities._AnalogOutputCapabilities.ValueType # 1 + ANALOG_OUTPUT_SUPPORTED: ClientIdentification.ClientCapabilities._AnalogOutputCapabilities.ValueType # 2 + ANALOG_OUTPUT_SUPPORTS_CGMS_A: ClientIdentification.ClientCapabilities._AnalogOutputCapabilities.ValueType # 3 + + class AnalogOutputCapabilities(_AnalogOutputCapabilities, metaclass=_AnalogOutputCapabilitiesEnumTypeWrapper): ... + ANALOG_OUTPUT_UNKNOWN: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities.ValueType # 0 + ANALOG_OUTPUT_NONE: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities.ValueType # 1 + ANALOG_OUTPUT_SUPPORTED: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities.ValueType # 2 + ANALOG_OUTPUT_SUPPORTS_CGMS_A: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities.ValueType # 3 + + class _WatermarkingSupport: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _WatermarkingSupportEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ClientIdentification.ClientCapabilities._WatermarkingSupport.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + WATERMARKING_SUPPORT_UNKNOWN: ClientIdentification.ClientCapabilities._WatermarkingSupport.ValueType # 0 + WATERMARKING_NOT_SUPPORTED: ClientIdentification.ClientCapabilities._WatermarkingSupport.ValueType # 1 + WATERMARKING_CONFIGURABLE: ClientIdentification.ClientCapabilities._WatermarkingSupport.ValueType # 2 + WATERMARKING_ALWAYS_ON: ClientIdentification.ClientCapabilities._WatermarkingSupport.ValueType # 3 + + class WatermarkingSupport(_WatermarkingSupport, metaclass=_WatermarkingSupportEnumTypeWrapper): ... + WATERMARKING_SUPPORT_UNKNOWN: ClientIdentification.ClientCapabilities.WatermarkingSupport.ValueType # 0 + WATERMARKING_NOT_SUPPORTED: ClientIdentification.ClientCapabilities.WatermarkingSupport.ValueType # 1 + WATERMARKING_CONFIGURABLE: ClientIdentification.ClientCapabilities.WatermarkingSupport.ValueType # 2 + WATERMARKING_ALWAYS_ON: ClientIdentification.ClientCapabilities.WatermarkingSupport.ValueType # 3 + + CLIENT_TOKEN_FIELD_NUMBER: builtins.int + SESSION_TOKEN_FIELD_NUMBER: builtins.int + VIDEO_RESOLUTION_CONSTRAINTS_FIELD_NUMBER: builtins.int + MAX_HDCP_VERSION_FIELD_NUMBER: builtins.int + OEM_CRYPTO_API_VERSION_FIELD_NUMBER: builtins.int + ANTI_ROLLBACK_USAGE_TABLE_FIELD_NUMBER: builtins.int + SRM_VERSION_FIELD_NUMBER: builtins.int + CAN_UPDATE_SRM_FIELD_NUMBER: builtins.int + SUPPORTED_CERTIFICATE_KEY_TYPE_FIELD_NUMBER: builtins.int + ANALOG_OUTPUT_CAPABILITIES_FIELD_NUMBER: builtins.int + CAN_DISABLE_ANALOG_OUTPUT_FIELD_NUMBER: builtins.int + RESOURCE_RATING_TIER_FIELD_NUMBER: builtins.int + WATERMARKING_SUPPORT_FIELD_NUMBER: builtins.int + INITIAL_RENEWAL_DELAY_BASE_FIELD_NUMBER: builtins.int + client_token: builtins.bool + session_token: builtins.bool + video_resolution_constraints: builtins.bool + max_hdcp_version: global___ClientIdentification.ClientCapabilities.HdcpVersion.ValueType + oem_crypto_api_version: builtins.int + anti_rollback_usage_table: builtins.bool + """Client has hardware support for protecting the usage table, such as + storing the generation number in secure memory. For Details, see: + Widevine Modular DRM Security Integration Guide for CENC + """ + srm_version: builtins.int + """The client shall report |srm_version| if available.""" + can_update_srm: builtins.bool + """A device may have SRM data, and report a version, but may not be capable + of updating SRM data. + """ + analog_output_capabilities: global___ClientIdentification.ClientCapabilities.AnalogOutputCapabilities.ValueType + can_disable_analog_output: builtins.bool + resource_rating_tier: builtins.int + """Clients can indicate a performance level supported by OEMCrypto. + This will allow applications and providers to choose an appropriate + quality of content to serve. Currently defined tiers are + 1 (low), 2 (medium) and 3 (high). Any other value indicates that + the resource rating is unavailable or reporting erroneous values + for that device. For details see, + Widevine Modular DRM Security Integration Guide for CENC + """ + watermarking_support: global___ClientIdentification.ClientCapabilities.WatermarkingSupport.ValueType + initial_renewal_delay_base: builtins.bool + @property + def supported_certificate_key_type(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___ClientIdentification.ClientCapabilities.CertificateKeyType.ValueType]: ... + def __init__( + self, + *, + client_token: builtins.bool | None = ..., + session_token: builtins.bool | None = ..., + video_resolution_constraints: builtins.bool | None = ..., + max_hdcp_version: global___ClientIdentification.ClientCapabilities.HdcpVersion.ValueType | None = ..., + oem_crypto_api_version: builtins.int | None = ..., + anti_rollback_usage_table: builtins.bool | None = ..., + srm_version: builtins.int | None = ..., + can_update_srm: builtins.bool | None = ..., + supported_certificate_key_type: collections.abc.Iterable[global___ClientIdentification.ClientCapabilities.CertificateKeyType.ValueType] | None = ..., + analog_output_capabilities: global___ClientIdentification.ClientCapabilities.AnalogOutputCapabilities.ValueType | None = ..., + can_disable_analog_output: builtins.bool | None = ..., + resource_rating_tier: builtins.int | None = ..., + watermarking_support: global___ClientIdentification.ClientCapabilities.WatermarkingSupport.ValueType | None = ..., + initial_renewal_delay_base: builtins.bool | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["analog_output_capabilities", b"analog_output_capabilities", "anti_rollback_usage_table", b"anti_rollback_usage_table", "can_disable_analog_output", b"can_disable_analog_output", "can_update_srm", b"can_update_srm", "client_token", b"client_token", "initial_renewal_delay_base", b"initial_renewal_delay_base", "max_hdcp_version", b"max_hdcp_version", "oem_crypto_api_version", b"oem_crypto_api_version", "resource_rating_tier", b"resource_rating_tier", "session_token", b"session_token", "srm_version", b"srm_version", "video_resolution_constraints", b"video_resolution_constraints", "watermarking_support", b"watermarking_support"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["analog_output_capabilities", b"analog_output_capabilities", "anti_rollback_usage_table", b"anti_rollback_usage_table", "can_disable_analog_output", b"can_disable_analog_output", "can_update_srm", b"can_update_srm", "client_token", b"client_token", "initial_renewal_delay_base", b"initial_renewal_delay_base", "max_hdcp_version", b"max_hdcp_version", "oem_crypto_api_version", b"oem_crypto_api_version", "resource_rating_tier", b"resource_rating_tier", "session_token", b"session_token", "srm_version", b"srm_version", "supported_certificate_key_type", b"supported_certificate_key_type", "video_resolution_constraints", b"video_resolution_constraints", "watermarking_support", b"watermarking_support"]) -> None: ... + + @typing.final + class ClientCredentials(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + TYPE_FIELD_NUMBER: builtins.int + TOKEN_FIELD_NUMBER: builtins.int + type: global___ClientIdentification.TokenType.ValueType + token: builtins.bytes + def __init__( + self, + *, + type: global___ClientIdentification.TokenType.ValueType | None = ..., + token: builtins.bytes | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["token", b"token", "type", b"type"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["token", b"token", "type", b"type"]) -> None: ... + + TYPE_FIELD_NUMBER: builtins.int + TOKEN_FIELD_NUMBER: builtins.int + CLIENT_INFO_FIELD_NUMBER: builtins.int + PROVIDER_CLIENT_TOKEN_FIELD_NUMBER: builtins.int + LICENSE_COUNTER_FIELD_NUMBER: builtins.int + CLIENT_CAPABILITIES_FIELD_NUMBER: builtins.int + VMP_DATA_FIELD_NUMBER: builtins.int + DEVICE_CREDENTIALS_FIELD_NUMBER: builtins.int + type: global___ClientIdentification.TokenType.ValueType + """Type of factory-provisioned device root of trust. Optional.""" + token: builtins.bytes + """Factory-provisioned device root of trust. Required.""" + provider_client_token: builtins.bytes + """Client token generated by the content provider. Optional.""" + license_counter: builtins.int + """Number of licenses received by the client to which the token above belongs. + Only present if client_token is specified. + """ + vmp_data: builtins.bytes + """Serialized VmpData message. Optional.""" + @property + def client_info(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ClientIdentification.NameValue]: + """Optional client information name/value pairs.""" + + @property + def client_capabilities(self) -> global___ClientIdentification.ClientCapabilities: + """List of non-baseline client capabilities.""" + + @property + def device_credentials(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ClientIdentification.ClientCredentials]: + """Optional field that may contain additional provisioning credentials.""" + + def __init__( + self, + *, + type: global___ClientIdentification.TokenType.ValueType | None = ..., + token: builtins.bytes | None = ..., + client_info: collections.abc.Iterable[global___ClientIdentification.NameValue] | None = ..., + provider_client_token: builtins.bytes | None = ..., + license_counter: builtins.int | None = ..., + client_capabilities: global___ClientIdentification.ClientCapabilities | None = ..., + vmp_data: builtins.bytes | None = ..., + device_credentials: collections.abc.Iterable[global___ClientIdentification.ClientCredentials] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["client_capabilities", b"client_capabilities", "license_counter", b"license_counter", "provider_client_token", b"provider_client_token", "token", b"token", "type", b"type", "vmp_data", b"vmp_data"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["client_capabilities", b"client_capabilities", "client_info", b"client_info", "device_credentials", b"device_credentials", "license_counter", b"license_counter", "provider_client_token", b"provider_client_token", "token", b"token", "type", b"type", "vmp_data", b"vmp_data"]) -> None: ... + +global___ClientIdentification = ClientIdentification + +@typing.final +class EncryptedClientIdentification(google.protobuf.message.Message): + """EncryptedClientIdentification message used to hold ClientIdentification + messages encrypted for privacy purposes. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PROVIDER_ID_FIELD_NUMBER: builtins.int + SERVICE_CERTIFICATE_SERIAL_NUMBER_FIELD_NUMBER: builtins.int + ENCRYPTED_CLIENT_ID_FIELD_NUMBER: builtins.int + ENCRYPTED_CLIENT_ID_IV_FIELD_NUMBER: builtins.int + ENCRYPTED_PRIVACY_KEY_FIELD_NUMBER: builtins.int + provider_id: builtins.str + """Provider ID for which the ClientIdentifcation is encrypted (owner of + service certificate). + """ + service_certificate_serial_number: builtins.bytes + """Serial number for the service certificate for which ClientIdentification is + encrypted. + """ + encrypted_client_id: builtins.bytes + """Serialized ClientIdentification message, encrypted with the privacy key + using AES-128-CBC with PKCS#5 padding. + """ + encrypted_client_id_iv: builtins.bytes + """Initialization vector needed to decrypt encrypted_client_id.""" + encrypted_privacy_key: builtins.bytes + """AES-128 privacy key, encrypted with the service public key using RSA-OAEP.""" + def __init__( + self, + *, + provider_id: builtins.str | None = ..., + service_certificate_serial_number: builtins.bytes | None = ..., + encrypted_client_id: builtins.bytes | None = ..., + encrypted_client_id_iv: builtins.bytes | None = ..., + encrypted_privacy_key: builtins.bytes | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["encrypted_client_id", b"encrypted_client_id", "encrypted_client_id_iv", b"encrypted_client_id_iv", "encrypted_privacy_key", b"encrypted_privacy_key", "provider_id", b"provider_id", "service_certificate_serial_number", b"service_certificate_serial_number"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["encrypted_client_id", b"encrypted_client_id", "encrypted_client_id_iv", b"encrypted_client_id_iv", "encrypted_privacy_key", b"encrypted_privacy_key", "provider_id", b"provider_id", "service_certificate_serial_number", b"service_certificate_serial_number"]) -> None: ... + +global___EncryptedClientIdentification = EncryptedClientIdentification + +@typing.final +class DrmCertificate(google.protobuf.message.Message): + """---------------------------------------------------------------------------- + drm_certificate.proto + ---------------------------------------------------------------------------- + Description of section: + Definition of the root of trust identifier proto. The proto message contains + the EC-IES encrypted identifier (e.g. keybox unique id) for a device and + an associated hash. These can be used by Widevine to identify the root of + trust that was used to acquire a DRM certificate. + + In addition to the encrypted part and the hash, the proto contains the + version of the root of trust id which implies the EC key algorithm that was + used. + + DRM certificate definition for user devices, intermediate, service, and root + certificates. + Next id: 13 + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + class _Type: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _TypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DrmCertificate._Type.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + ROOT: DrmCertificate._Type.ValueType # 0 + """ProtoBestPractices: ignore.""" + DEVICE_MODEL: DrmCertificate._Type.ValueType # 1 + DEVICE: DrmCertificate._Type.ValueType # 2 + SERVICE: DrmCertificate._Type.ValueType # 3 + PROVISIONER: DrmCertificate._Type.ValueType # 4 + + class Type(_Type, metaclass=_TypeEnumTypeWrapper): ... + ROOT: DrmCertificate.Type.ValueType # 0 + """ProtoBestPractices: ignore.""" + DEVICE_MODEL: DrmCertificate.Type.ValueType # 1 + DEVICE: DrmCertificate.Type.ValueType # 2 + SERVICE: DrmCertificate.Type.ValueType # 3 + PROVISIONER: DrmCertificate.Type.ValueType # 4 + + class _ServiceType: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _ServiceTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DrmCertificate._ServiceType.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNKNOWN_SERVICE_TYPE: DrmCertificate._ServiceType.ValueType # 0 + LICENSE_SERVER_SDK: DrmCertificate._ServiceType.ValueType # 1 + LICENSE_SERVER_PROXY_SDK: DrmCertificate._ServiceType.ValueType # 2 + PROVISIONING_SDK: DrmCertificate._ServiceType.ValueType # 3 + CAS_PROXY_SDK: DrmCertificate._ServiceType.ValueType # 4 + + class ServiceType(_ServiceType, metaclass=_ServiceTypeEnumTypeWrapper): ... + UNKNOWN_SERVICE_TYPE: DrmCertificate.ServiceType.ValueType # 0 + LICENSE_SERVER_SDK: DrmCertificate.ServiceType.ValueType # 1 + LICENSE_SERVER_PROXY_SDK: DrmCertificate.ServiceType.ValueType # 2 + PROVISIONING_SDK: DrmCertificate.ServiceType.ValueType # 3 + CAS_PROXY_SDK: DrmCertificate.ServiceType.ValueType # 4 + + class _Algorithm: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _AlgorithmEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DrmCertificate._Algorithm.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNKNOWN_ALGORITHM: DrmCertificate._Algorithm.ValueType # 0 + RSA: DrmCertificate._Algorithm.ValueType # 1 + ECC_SECP256R1: DrmCertificate._Algorithm.ValueType # 2 + ECC_SECP384R1: DrmCertificate._Algorithm.ValueType # 3 + ECC_SECP521R1: DrmCertificate._Algorithm.ValueType # 4 + + class Algorithm(_Algorithm, metaclass=_AlgorithmEnumTypeWrapper): ... + UNKNOWN_ALGORITHM: DrmCertificate.Algorithm.ValueType # 0 + RSA: DrmCertificate.Algorithm.ValueType # 1 + ECC_SECP256R1: DrmCertificate.Algorithm.ValueType # 2 + ECC_SECP384R1: DrmCertificate.Algorithm.ValueType # 3 + ECC_SECP521R1: DrmCertificate.Algorithm.ValueType # 4 + + @typing.final + class EncryptionKey(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + PUBLIC_KEY_FIELD_NUMBER: builtins.int + ALGORITHM_FIELD_NUMBER: builtins.int + public_key: builtins.bytes + """Device public key. PKCS#1 ASN.1 DER-encoded. Required.""" + algorithm: global___DrmCertificate.Algorithm.ValueType + """Required. The algorithm field contains the curve used to create the + |public_key| if algorithm is one of the ECC types. + The |algorithm| is used for both to determine the if the certificate is + ECC or RSA. The |algorithm| also specifies the parameters that were used + to create |public_key| and are used to create an ephemeral session key. + """ + def __init__( + self, + *, + public_key: builtins.bytes | None = ..., + algorithm: global___DrmCertificate.Algorithm.ValueType | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["algorithm", b"algorithm", "public_key", b"public_key"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["algorithm", b"algorithm", "public_key", b"public_key"]) -> None: ... + + TYPE_FIELD_NUMBER: builtins.int + SERIAL_NUMBER_FIELD_NUMBER: builtins.int + CREATION_TIME_SECONDS_FIELD_NUMBER: builtins.int + EXPIRATION_TIME_SECONDS_FIELD_NUMBER: builtins.int + PUBLIC_KEY_FIELD_NUMBER: builtins.int + SYSTEM_ID_FIELD_NUMBER: builtins.int + TEST_DEVICE_DEPRECATED_FIELD_NUMBER: builtins.int + PROVIDER_ID_FIELD_NUMBER: builtins.int + SERVICE_TYPES_FIELD_NUMBER: builtins.int + ALGORITHM_FIELD_NUMBER: builtins.int + ROT_ID_FIELD_NUMBER: builtins.int + ENCRYPTION_KEY_FIELD_NUMBER: builtins.int + type: global___DrmCertificate.Type.ValueType + """Type of certificate. Required.""" + serial_number: builtins.bytes + """128-bit globally unique serial number of certificate. + Value is 0 for root certificate. Required. + """ + creation_time_seconds: builtins.int + """POSIX time, in seconds, when the certificate was created. Required.""" + expiration_time_seconds: builtins.int + """POSIX time, in seconds, when the certificate should expire. Value of zero + denotes indefinite expiry time. For more information on limited lifespan + DRM certificates see (go/limited-lifespan-drm-certificates). + """ + public_key: builtins.bytes + """Device public key. PKCS#1 ASN.1 DER-encoded. Required.""" + system_id: builtins.int + """Widevine system ID for the device. Required for intermediate and + user device certificates. + """ + test_device_deprecated: builtins.bool + """Deprecated field, which used to indicate whether the device was a test + (non-production) device. The test_device field in ProvisionedDeviceInfo + below should be observed instead. + """ + provider_id: builtins.str + """Service identifier (web origin) for the provider which owns the + certificate. Required for service and provisioner certificates. + """ + algorithm: global___DrmCertificate.Algorithm.ValueType + """Required. The algorithm field contains the curve used to create the + |public_key| if algorithm is one of the ECC types. + The |algorithm| is used for both to determine the if the certificate is ECC + or RSA. The |algorithm| also specifies the parameters that were used to + create |public_key| and are used to create an ephemeral session key. + """ + rot_id: builtins.bytes + """Optional. May be present in DEVICE certificate types. This is the root + of trust identifier that holds an encrypted value that identifies the + keybox or other root of trust that was used to provision a DEVICE drm + certificate. + """ + @property + def service_types(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[global___DrmCertificate.ServiceType.ValueType]: + """This field is used only when type = SERVICE to specify which SDK uses + service certificate. This repeated field is treated as a set. A certificate + may be used for the specified service SDK if the appropriate ServiceType + is specified in this field. + """ + + @property + def encryption_key(self) -> global___DrmCertificate.EncryptionKey: + """Optional. May be present in devices that explicitly support dual keys. When + present the |public_key| is used for verification of received license + request messages. + """ + + def __init__( + self, + *, + type: global___DrmCertificate.Type.ValueType | None = ..., + serial_number: builtins.bytes | None = ..., + creation_time_seconds: builtins.int | None = ..., + expiration_time_seconds: builtins.int | None = ..., + public_key: builtins.bytes | None = ..., + system_id: builtins.int | None = ..., + test_device_deprecated: builtins.bool | None = ..., + provider_id: builtins.str | None = ..., + service_types: collections.abc.Iterable[global___DrmCertificate.ServiceType.ValueType] | None = ..., + algorithm: global___DrmCertificate.Algorithm.ValueType | None = ..., + rot_id: builtins.bytes | None = ..., + encryption_key: global___DrmCertificate.EncryptionKey | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["algorithm", b"algorithm", "creation_time_seconds", b"creation_time_seconds", "encryption_key", b"encryption_key", "expiration_time_seconds", b"expiration_time_seconds", "provider_id", b"provider_id", "public_key", b"public_key", "rot_id", b"rot_id", "serial_number", b"serial_number", "system_id", b"system_id", "test_device_deprecated", b"test_device_deprecated", "type", b"type"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["algorithm", b"algorithm", "creation_time_seconds", b"creation_time_seconds", "encryption_key", b"encryption_key", "expiration_time_seconds", b"expiration_time_seconds", "provider_id", b"provider_id", "public_key", b"public_key", "rot_id", b"rot_id", "serial_number", b"serial_number", "service_types", b"service_types", "system_id", b"system_id", "test_device_deprecated", b"test_device_deprecated", "type", b"type"]) -> None: ... + +global___DrmCertificate = DrmCertificate + +@typing.final +class SignedDrmCertificate(google.protobuf.message.Message): + """---------------------------------------------------------------------------- + signed_drm_certificate.proto + ---------------------------------------------------------------------------- + Description of section: + DrmCertificate signed by a higher (CA) DRM certificate. + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DRM_CERTIFICATE_FIELD_NUMBER: builtins.int + SIGNATURE_FIELD_NUMBER: builtins.int + SIGNER_FIELD_NUMBER: builtins.int + HASH_ALGORITHM_FIELD_NUMBER: builtins.int + drm_certificate: builtins.bytes + """Serialized certificate. Required.""" + signature: builtins.bytes + """Signature of certificate. Signed with root or intermediate + certificate specified below. Required. + """ + hash_algorithm: global___HashAlgorithmProto.ValueType + """Optional field that indicates the hash algorithm used in signature scheme.""" + @property + def signer(self) -> global___SignedDrmCertificate: + """SignedDrmCertificate used to sign this certificate.""" + + def __init__( + self, + *, + drm_certificate: builtins.bytes | None = ..., + signature: builtins.bytes | None = ..., + signer: global___SignedDrmCertificate | None = ..., + hash_algorithm: global___HashAlgorithmProto.ValueType | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["drm_certificate", b"drm_certificate", "hash_algorithm", b"hash_algorithm", "signature", b"signature", "signer", b"signer"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["drm_certificate", b"drm_certificate", "hash_algorithm", b"hash_algorithm", "signature", b"signature", "signer", b"signer"]) -> None: ... + +global___SignedDrmCertificate = SignedDrmCertificate diff --git a/keydive/keybox.py b/keydive/keybox.py deleted file mode 100644 index e707b24..0000000 --- a/keydive/keybox.py +++ /dev/null @@ -1,180 +0,0 @@ -import base64 -import json -import logging - -from json.encoder import encode_basestring_ascii -from typing import Literal -from uuid import UUID -from pathlib import Path - - -def bytes2int(value: bytes, byteorder: Literal["big", "little"] = "big", signed: bool = False) -> int: - """ - Convert a byte sequence to an integer. - - Parameters: - value (bytes): The byte sequence to convert. - byteorder (str, optional): Byte order for conversion. 'big' or 'little'. Defaults to 'big'. - signed (bool, optional): Whether the integer is signed. Defaults to False. - - Returns: - int: The integer representation of the byte sequence. - """ - return int.from_bytes(value, byteorder=byteorder, signed=signed) - - -class Keybox: - """ - The Keybox class handles the storage and management of device IDs and keybox data. - """ - - def __init__(self): - """ - Initializes the Keybox object, setting up logger and containers for device IDs and keyboxes. - """ - self.logger = logging.getLogger(self.__class__.__name__) - # https://github.com/kaltura/kaltura-device-info-android/blob/master/app/src/main/java/com/kaltura/kalturadeviceinfo/MainActivity.java#L203 - self.device_id = [] - self.keybox = {} - - def set_device_id(self, data: bytes) -> None: - """ - Set the device ID from the provided data. - - Parameters: - data (bytes): The device ID, expected to be 32 bytes long. - - Raises: - AssertionError: If the data length is not 32 bytes. - """ - try: - size = len(data) - # Ensure the device ID is exactly 32 bytes long - assert size == 32, f"Invalid device ID length: {size}. Should be 32 bytes" - - # Add device ID to the list if it's not already present - if data not in self.device_id: - self.logger.info("Receive device id: \n\n%s\n", encode_basestring_ascii(data.decode("utf-8"))) - self.device_id.append(data) - - except Exception as e: - self.logger.debug("Failed to set device id: %s", e) - - def set_keybox(self, data: bytes) -> None: - """ - Set the keybox from the provided data. - - Parameters: - data (bytes): The keybox data, expected to be either 128 or 132 bytes long. - - Raises: - AssertionError: If the data length is not 128 or 132 bytes or does not meet other criteria. - """ - # https://github.com/zybpp/Python/tree/master/Python/keybox - try: - size = len(data) - # Validate the keybox size (128 or 132 bytes) - assert size in (128, 132), f"Invalid keybox length: {size}. Should be 128 or 132 bytes" - - # Validate the QSEE-style keybox end - assert size == 128 or data[128:132] == b"LVL1", "QSEE-style keybox must end with bytes 'LVL1'" - - # Validate the keybox magic (should be 'kbox') - assert data[120:124] == b"kbox", "Invalid keybox magic" - - device_id = data[0:32] # Extract the device ID from the first 32 bytes - - # Retrieve and log the structured keybox information - infos = self.__keybox_info(data) - encrypted = infos["flags"] > 10 # Check if the keybox is encrypted - self.set_device_id(data=device_id) # Set the device ID - - # Log and store the keybox data if it's a new keybox or the device ID is updated - if (device_id in self.keybox and self.keybox[device_id] != (data, encrypted)) or device_id not in self.keybox: - self.logger.info("Receive keybox: \n\n%s\n", json.dumps(infos, indent=2)) - - # Warn if keybox is encrypted and interception of plaintext device token is needed - if encrypted: - self.logger.warning("Keybox contains encrypted data. Interception of plaintext device token is needed") - - # Store the keybox (encrypted or not) for the device ID - if (device_id in self.keybox and not encrypted) or device_id not in self.keybox: - self.keybox[device_id] = (data, encrypted) - except Exception as e: - self.logger.debug("Failed to set keybox: %s", e) - - @staticmethod - def __keybox_info(data: bytes) -> dict: - """ - Extract keybox information from the provided data. - - Parameters: - data (bytes): The keybox data. - - Returns: - dict: A dictionary containing extracted keybox information. - """ - # https://github.com/wvdumper/dumper/blob/main/Helpers/Keybox.py#L51 - - # Extract device-specific information from the keybox data - device_token = data[48:120] - - # Prepare the keybox content dictionary - content = { - "device_id": data[0:32].decode("utf-8"), # Device's unique identifier (32 bytes) - "device_key": data[32:48], # Device cryptographic key (16 bytes) - "device_token": device_token, # Token for device authentication (72 bytes) - "keybox_tag": data[120:124].decode("utf-8"), # Magic tag (4 bytes) - "crc32": bytes2int(data[124:128]), # CRC32 checksum (4 bytes) - "level_tag": data[128:132].decode("utf-8") or None, # Optional level tag (4 bytes) - - # Extract metadata from the device token (Bytes 48–120) - "flags": bytes2int(device_token[0:4]), # Device flags (4 bytes) - "system_id": bytes2int(device_token[4:8]), # System identifier (4 bytes) - "provisioning_id": UUID(bytes_le=device_token[8:24]), # Provisioning UUID (16 bytes) - "encrypted_bits": device_token[24:72] # Encrypted device-specific information (48 bytes) - } - - # https://github.com/ThatNotEasy/Parser-DRM/blob/main/modules/widevine.py#L84 - # TODO: decrypt device token value - - # Encode bytes as base64 and convert UUIDs to string - return { - k: base64.b64encode(v).decode("utf-8") if isinstance(v, bytes) else str(v) if isinstance(v, UUID) else v - for k, v in content.items() - } - - def export(self, parent: Path) -> bool: - """ - Export the keybox data to a file in the specified parent directory. - - Parameters: - parent (Path): The parent directory where the keybox data will be saved. - - Returns: - bool: True if any keybox were exported, otherwise False. - """ - # Find matching keyboxes based on the device_id - keys = self.device_id & self.keybox.keys() - - for k in keys: - # Create the parent directory if it doesn't exist - parent.mkdir(parents=True, exist_ok=True) - - # Define the export file path and extension (encrypted or binary) - path_keybox_bin = parent / ("keybox." + ("enc" if self.keybox[k][1] else "bin")) - - # Write the keybox data to the file - path_keybox_bin.write_bytes(self.keybox[k][0]) - - # Log export status based on whether the keybox is encrypted - if self.keybox[k][1]: - self.logger.warning("Exported encrypted keybox: %s", path_keybox_bin) - else: - self.logger.info("Exported keybox: %s", path_keybox_bin) - - # Return True if any keyboxes were exported, otherwise False - return len(keys) > 0 - - -__all__ = ("Keybox",) diff --git a/keydive/keydive.js b/keydive/keydive.js index a5e36db..bfe33c1 100644 --- a/keydive/keydive.js +++ b/keydive/keydive.js @@ -1,5 +1,5 @@ /** - * Date: 2025-03-01 + * Date: 2025-06-09 * Description: DRM key extraction for research and educational purposes. * Source: https://github.com/hyugogirubato/KeyDive */ @@ -8,7 +8,9 @@ const OEM_CRYPTO_API = JSON.parse('${OEM_CRYPTO_API}'); const NATIVE_C_API = JSON.parse('${NATIVE_C_API}'); const SYMBOLS = JSON.parse('${SYMBOLS}'); -const SKIP = '${SKIP}' === 'True'; +const DETECT = '${DETECT}' === 'True'; +const DISABLER = '${DISABLER}' === 'True'; +const UNENCRYPT = '${UNENCRYPT}' === 'True'; // Logging levels to synchronize with Python's logging module. @@ -23,40 +25,100 @@ const Level = { CRITICAL: 50 }; -// Utility for encoding strings into byte arrays (UTF-8). -// https://gist.github.com/Yaffle/5458286#file-textencodertextdecoder-js -function TextEncoder() { +// Backward compatibility with the modern equivalent implemented since frida 17 +// https://frida.re/news/2025/05/17/frida-17-0-0-released/ +/* +readS8 = readShort +readU8 = readUShort +readS16 = readInt +readU16 = readUInt +readS32 = readFloat +readU32 = readDouble + +console.log(hexdump(address, { + offset: 0, + length: 128, + header: true, + ansi: true +})); + */ +Memory = typeof Memory === 'undefined' ? {} : Memory; +Memory.readByteArray ??= (address, length) => address.readByteArray(length); +Memory.readPointer ??= (address) => address.readPointer(); +Memory.readU16 ??= (address) => address.readU16(); + +Memory.readStdString = function (address) { + // https://learnfrida.info/intermediate_usage/#stdstring + // Read string size (2 bytes) at offset pointerSize + const size = Memory.readU16(address.add(Process.pointerSize)); + + // Check if string is using Small String Optimization (SSO) + const LSB = address.readU8() & 1; + if (LSB === 0) { + // https://codeshare.frida.re/@oleavr/read-std-string/ + // SSO: data is stored inline starting at address + 1 + return Memory.readByteArray(address.add(1), size); + } else { + // Non-SSO: pointer to data is stored at address + return Memory.readByteArray(address.add(Process.pointerSize * 2).readPointer(), size); + } } -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; +Memory.readStdVector = function (address) { + // https://learnfrida.info/intermediate_usage/#stdvector + // Read the vector size (2 bytes) at offset pointerSize + let size = Memory.readU16(address.add(Process.pointerSize)); + + // Read pointer to the start of the vector data (at offset 0) + const data = Memory.readByteArray(address.readPointer(), size); + /* + const buffer = new Uint8Array(data); + + // Trim trailing null bytes (0x00) from the end + while (size > 0 && buffer[size - 1] === 0x00) { + size--; } - return octets; -}; + + // Return the trimmed buffer + return buffer.slice(0, size); + */ + return data; +} + +// Utility for encoding strings into byte arrays (UTF-8). +// https://gist.github.com/Yaffle/5458286#file-textencodertextdecoder-js +class TextEncoder { + + encode(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) => { @@ -69,7 +131,7 @@ const getVersion = () => Frida.version; // @Utils -const getLibraries = () => { +function getLibraries() { // https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792 try { return Process.enumerateModules(); @@ -79,55 +141,94 @@ const getLibraries = () => { } } -const getLibrary = (name) => { - const libraries = getLibraries().filter(l => l.name === name); - return libraries.length === 1 ? libraries[0] : undefined; +function getLibrary(name) { + return getLibraries().find(l => l.name === name); } -const getFunctions = (library, dynamic) => { +function getFunctions(library, dynamic = false) { try { // https://frida.re/news/2025/01/09/frida-16-6-0-released/ - const functions = dynamic ? library.enumerateSymbols().map(item => ({ - type: item.type, - name: item.name, - address: item.address - })) : []; + const functions = dynamic + ? library.enumerateSymbols().map(item => ({ + type: item.type, + name: item.name, + address: + item.address + })) : []; - library.enumerateExports().forEach(item => { - if (!functions.includes(item)) { - functions.push(item); - } - }); - - return functions; + return functions.concat(library.enumerateExports()); } catch (e) { print(Level.CRITICAL, e.message); return []; } } -const disableLibrary = (name) => { +function getDerLength(buffer) { + let pos = 1; // Skip the tag byte (usually 0x30 for SEQUENCE) + + let lengthByte = buffer[pos++]; // Read length descriptor + + if (lengthByte < 0x80) { + // Short form: length is in this byte + return pos + lengthByte; + } + + // Long form: next N bytes encode the length + const numLenBytes = lengthByte & 0x7F; + + if (numLenBytes + pos > buffer.length) { + throw new Error('DER length bytes exceed buffer size'); + } + + let lengthValue = 0; + for (let i = 0; i < numLenBytes; i++) { + lengthValue = (lengthValue << 8) + buffer[pos++]; // Accumulate length + } + + // Total length = current pos (after length bytes) + value length + return pos + lengthValue; +} + +function readDerKey(address, size) { + // Read initial bytes from memory + const data = Memory.readByteArray(address, size); + const buffer = new Uint8Array(data); + + const tag = buffer[0]; // Usually 0x30 for SEQUENCE + + // Check if tag indicates a DER SEQUENCE + if (tag !== 0x30) return; + + try { + // Adjust size based on DER length field + size = getDerLength(buffer); // ASN.1 DER + } catch (e) { + // Ignore error, keep initial size + } + + // Return the DER key slice + return buffer.slice(0, size); +} + +function disableLibrary(name) { // Disables all functions in the specified library by replacing their implementations. const library = getLibrary(name); if (library) { // https://github.com/hyugogirubato/KeyDive/issues/23#issuecomment-2230374415 const functions = getFunctions(library, false); - const disabled = []; + const disabled = new Set(); - functions.forEach(func => { - const {name: funcName, address: funcAddr} = func; - if (func.type !== 'function' || disabled.includes(funcAddr)) return; + functions.forEach(({name: funcName, address: funcAddr, type}) => { + if (type !== 'function' || disabled.has(funcAddr)) return; try { - Interceptor.replace(funcAddr, new NativeCallback(function () { - return 0; - }, 'int', [])); - - disabled.push(funcAddr); + Interceptor.replace(funcAddr, new NativeCallback(() => ptr(0), 'pointer', [])); + disabled.add(funcAddr); } catch (e) { print(Level.DEBUG, `${e.message} for ${funcName}`); } }); + print(Level.INFO, `Library ${library.name} (${library.path}) has been disabled`); } else { print(Level.DEBUG, `Library ${name} was not found`); @@ -136,259 +237,491 @@ const disableLibrary = (name) => { // @Libraries -const UsePrivacyMode = (address) => { +function Properties_UsePrivacyMode(address) { /* wvcdm::Properties::UsePrivacyMode Args: - args[0]: std::string const& + args[1]: const CdmSessionId& session_id + Return: + retval: bool */ - Interceptor.replace(address, new NativeCallback(function () { - return 0; - }, 'int', [])); + Interceptor.replace(address, new NativeCallback(() => 0, 'bool', [])); Interceptor.attach(address, { onEnter: function (args) { - print(Level.DEBUG, '[+] onEnter: UsePrivacyMode'); + print(Level.DEBUG, '[+] onEnter: Properties::UsePrivacyMode'); }, onLeave: function (retval) { - print(Level.DEBUG, '[-] onLeave: UsePrivacyMode'); + print(Level.DEBUG, '[-] onLeave: Properties::UsePrivacyMode'); } }); } -const GetCdmClientPropertySet = (address) => { +function Properties_GetCdmClientPropertySet(address) { /* wvcdm::Properties::GetCdmClientPropertySet Args: - args[0]: std::string const& + args[1]: const CdmSessionId& session_id + Return: + retval: wvcdm::CdmClientPropertySet* */ - Interceptor.replace(address, new NativeCallback(function () { - return 0; - }, 'int', [])); + Interceptor.replace(address, new NativeCallback(() => ptr(0), 'pointer', [])); Interceptor.attach(address, { onEnter: function (args) { - print(Level.DEBUG, '[+] onEnter: GetCdmClientPropertySet'); + print(Level.DEBUG, '[+] onEnter: Properties::GetCdmClientPropertySet'); }, onLeave: function (retval) { - print(Level.DEBUG, '[-] onLeave: GetCdmClientPropertySet'); + print(Level.DEBUG, '[-] onLeave: Properties::GetCdmClientPropertySet'); } }); } -const PrepareKeyRequest = (address) => { +function CdmLicense_PrepareKeyRequest(address) { /* wvcdm::CdmLicense::PrepareKeyRequest Args: - args[0]: wvcdm::CdmLicense *this - args[1]: wvcdm::InitializationData const& - args[2]: wvcdm::CdmLicenseType - args[3]: std::map const& - args[5]: std::string* - args[6]: std::string* + args[1]: const InitializationData& init_data + args[2]: const std::string& client_token + args[3]: CdmLicenseType license_type + args[4]: const CdmAppParameterMap& app_parameters + args[5]: CdmKeyMessage* signed_request + args[6]: std::string* server_url + Return: + retval: wvcdm::CdmResponseType */ Interceptor.attach(address, { onEnter: function (args) { - print(Level.DEBUG, '[+] onEnter: PrepareKeyRequest'); + print(Level.DEBUG, '[+] onEnter: CdmLicense::PrepareKeyRequest'); // https://github.com/hyugogirubato/KeyDive/issues/13#issue-2327487249 this.params = []; - for (let i = 0; i < 7; i++) { + for (let i = 0; i < 8; i++) { this.params.push(args[i]); } }, onLeave: function (retval) { - print(Level.DEBUG, '[-] onLeave: PrepareKeyRequest'); + print(Level.DEBUG, '[-] onLeave: CdmLicense::PrepareKeyRequest'); let dumped = false; + // Extract and dump the relevant arguments for (let i = 0; i < this.params.length; i++) { + // Extract the signed_request data (CdmKeyMessage*) 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) { + const signedRequestData = Memory.readStdString(this.params[i]); + if (signedRequestData) { dumped = true; - send('challenge', data); + send('challenge', signedRequestData); } } catch (e) { - // print(Level.WARNING, `Failed to dump data for arg ${i}`); + // print(Level.WARNING, `Failed to extract signed_request data from args[${i}]`); } } - !dumped && print(Level.ERROR, 'Failed to dump challenge'); + !dumped && print(Level.ERROR, 'Failed to dump challenge data'); } }); } -const LoadDeviceRSAKey = (address, name) => { - // wvdash::OEMCrypto::LoadDeviceRSAKey - 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, `[*] LoadDeviceRSAKey: ${name}`); - send({'private_key': name}, key); - } - } - } - }, - 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. +function CdmEngine_GenerateKeyRequest(address) { /* - 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::_oecc07 + wvcdm::CdmEngine::GenerateKeyRequest Args: - args[0]: uchar * - args[1]: ulong * - args[3]: wvcdm::SecurityLevel + args[1]: const CdmSessionId& session_id + args[2]: const CdmKeySetId& key_set_id + args[3]: const InitializationData& init_data + args[4]: const CdmLicenseType license_type + args[5]: CdmAppParameterMap& app_parameters + args[6]: CdmKeyRequest* key_request + Return: + retval: wvcdm::CdmResponseType */ Interceptor.attach(address, { onEnter: function (args) { - // print(Level.DEBUG, '[+] onEnter: GetDeviceId'); - this.data = args[0]; - this.size = args[1]; + print(Level.DEBUG, '[+] onEnter: CdmEngine::GenerateKeyRequest'); + + // https://github.com/hyugogirubato/KeyDive/issues/13#issue-2327487249 + this.params = []; + for (let i = 0; i < 8; i++) { + this.params.push(args[i]); + } }, onLeave: function (retval) { - // print(Level.DEBUG, '[-] onLeave: GetDeviceId'); - const size = Memory.readPointer(this.size).toInt32(); - const data = Memory.readByteArray(this.data, size); + print(Level.DEBUG, '[-] onLeave: CdmEngine::GenerateKeyRequest'); + let dumped = false; - if (data) { - print(Level.DEBUG, `[*] GetDeviceId: ${name}`); - send('device_id', data); + // Extract and dump the relevant arguments + for (let i = 0; i < this.params.length; i++) { + // Extract the signed_request data (CdmKeyMessage*) + try { + const signedRequestData = Memory.readStdString(this.params[i]); + if (signedRequestData) { + dumped = true; + send('challenge', signedRequestData); + } + } catch (e) { + // print(Level.WARNING, `Failed to extract signed_request data from args[${i}]`); + } + } + !dumped && print(Level.ERROR, 'Failed to dump challenge data'); + } + }); +} + +function AesCbcKey_Encrypt(address) { + /* + wvcdm::AesCbcKey::Encrypt + + Args: + args[1]: const std::string& in + args[2]: std::string* out + args[3]: std::string* iv + Return: + retval: bool + */ + Interceptor.attach(address, { + onEnter: function (args) { + const inData = Memory.readStdString(args[1]); + if (inData) { + print(Level.DEBUG, '[*] AesCbcKey::Encrypt'); + send('client_id', inData); } } }); } -const FileSystemRead = (address) => { +function FileSystem_Read(address) { /* wvoec3::OEMCrypto_Level3AndroidFileSystem::Read Args: - args[0]: wvoec3::OEMCrypto_Level3AndroidFileSystem *this - args[1]: char const* - args[2]: void * - args[3]: ulong + args[1]: const char *filename + args[2]: void *buffer + args[3]: size_t size + Return: + retval: ssize_t */ Interceptor.attach(address, { onEnter: function (args) { - // print(Level.DEBUG, '[+] onEnter: FileSystemRead'); + const bufferPtr = args[2]; const size = args[3].toInt32(); - const data = Memory.readByteArray(args[2], size); + const data = Memory.readByteArray(bufferPtr, size); // Check if the size matches known keybox sizes (128 or 132 bytes) if ([128, 132].includes(size) && data) { - print(Level.DEBUG, '[*] FileSystemRead'); + print(Level.DEBUG, '[*] FileSystem::Read'); send('keybox', data); } - }, - onLeave: function (retval) { - // print(Level.DEBUG, '[-] onLeave: FileSystemRead'); } }); } -const FileRead = (address, name) => { +function File_Read(address) { /* wvcdm::File::Read Args: - args[0]: wvcdm::File *this - args[1]: char * - args[2]: uint + args[1]: char* buffer + args[2]: size_t bytes + Return: + retval: ssize_t */ /* _x1c36 Args: - args[0]: char *filename - args[1]: void *ptr - args[2]: size_t n + args[0]: std::string* filename + args[1]: char* buffer + args[2]: size_t bytes + Return: + retval: ssize_t */ Interceptor.attach(address, { onEnter: function (args) { - // print(Level.DEBUG, `[+] onEnter: FileRead: ${name}`); + const bufferPtr = args[1]; const size = args[2].toInt32(); - const data = Memory.readByteArray(args[1], size); + const data = Memory.readByteArray(bufferPtr, size); // Check if the size matches known keybox sizes (128 or 132 bytes) if ([128, 132].includes(size) && data) { - print(Level.DEBUG, `[*] FileRead: ${name}`); + print(Level.DEBUG, '[*] File::Read'); send('keybox', data); } - }, - onLeave: function (retval) { - // print(Level.DEBUG, `[-] onLeave: FileRead: ${name}`); } }); } -const RunningCRC = (address) => { +function RunningCRC(address) { /* - wvrunningcrc32 + wvoec::wvrunningcrc32 Args: - args[0]: uchar const* - args[1]: int - args[2]: uint + args[0]: const uint8_t* p_begin + args[1]: int i_count + args[2]: uint32_t i_crc + Return: + retval: uint32_t */ Interceptor.attach(address, { onEnter: function (args) { - // print(Level.DEBUG, '[+] onEnter: RunningCRC'); const size = args[1].toInt32(); + const data = Memory.readByteArray(args[0], 128); // Check if size matches keybox length excluding 4-byte magic/tag fields - if (size === 124) { - const data = Memory.readByteArray(args[0], 128); + if (size === 124 && data) { print(Level.DEBUG, '[*] RunningCRC'); send('keybox', data); } + } + }); +} + +function OEMCrypto_GetDeviceID(address) { + /* + wvcdm::OEMCrypto_GetDeviceID + + Args: + args[0]: uint8_t* deviceID + args[1]: size_t* idLength + args[2]: SecurityLevel level + Return: + retval: OEMCryptoResult + */ + Interceptor.attach(address, { + onEnter: function (args) { + this.deviceIdPtr = args[0]; + this.idLengthPtr = args[1]; }, onLeave: function (retval) { - // print(Level.DEBUG, '[-] onLeave: RunningCRC'); + const idLength = Memory.readPointer(this.idLengthPtr).toInt32(); + const deviceIdData = Memory.readByteArray(this.deviceIdPtr, idLength); + + if (deviceIdData) { + print(Level.DEBUG, '[*] OEMCrypto_GetDeviceID'); + send('stable_id', deviceIdData); + } + } + }); +} + +function OEMCrypto_GetKeyData(address) { + /* + wvcdm::OEMCrypto_GetKeyData + + Args: + args[0]: uint8_t* keyData + args[1]: size_t* keyDataLength + args[2]: SecurityLevel level + Return: + retval: OEMCryptoResult + */ + Interceptor.attach(address, { + onEnter: function (args) { + this.keyDataPtr = args[0]; + this.keyDataLengthPtr = args[1]; + }, + onLeave: function (retval) { + const keyDataLength = Memory.readPointer(this.keyDataLengthPtr).toInt32(); + const keyData = Memory.readByteArray(this.keyDataPtr, keyDataLength); + + if (keyData) { + print(Level.DEBUG, '[*] OEMCrypto_GetKeyData'); + send('device_id', keyData); + } + } + }); +} + +function OEMCrypto_ProvisioningMethod(address) { + /* + wvcdm::OEMCrypto_GetProvisioningMethod + + Args: + args[0]: SecurityLevel level + Return: + retval: OEMCrypto_ProvisioningMethod + */ + Interceptor.attach(address, { + onLeave: function (retval) { + // https://github.com/fox0618/dumper/blob/main/Helpers/script.js#L784 + print(Level.DEBUG, '[*] OEMCrypto_ProvisioningMethod'); + send('provisioning_method', new TextEncoder().encode(`${retval.toInt32()}`)); + } + }); +} + +function OEMCrypto_GenerateDerivedKeys(address) { + /* + wvcdm::OEMCrypto_GenerateDerivedKeys + + Args: + args[0]: OEMCrypto_SESSION session + args[1]: const uint8_t* mac_key_context + args[2]: uint32_t mac_key_context_length + args[3]: const uint8_t* enc_key_context, + args[4]: uint32_t enc_key_context_length + Return: + retval: OEMCryptoResult + */ + Interceptor.attach(address, { + onEnter: function (args) { + print(Level.DEBUG, '[+] onEnter: OEMCrypto_GenerateDerivedKeys'); + // https://github.com/Avalonswanderer/widevinel3_Android_PoC/blob/main/PoCs/content_key_recovery.py#L103C55-L103C72 + const macKeyContext = Memory.readByteArray(args[1], args[2].toInt32()); + const encKeyContext = Memory.readByteArray(args[3], args[4].toInt32()); + + // Used for L1 provisioning + // console.log('macKeyContext:', macKeyContext); + /* + macKeyContext: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF + 00000000 41 55 54 48 45 4e 54 49 43 41 54 49 4f 4e 00 12 AUTHENTICATION.. + 00000010 04 0a d5 8e c0 1a 04 08 00 12 00 2a 98 06 0a 0c ...........*.... + 00000020 77 69 64 65 76 69 6e 65 2e 63 6f 6d 12 10 51 43 widevine.com..QC + 00000030 4f e2 a4 4c 76 3b cc 2c 82 6a 2d 6e f9 a7 1a e0 O..Lv;.,.j-n.... + */ + + // console.log('encKeyContext:', encKeyContext); + /* + encKeyContext: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF + 00000000 45 4e 43 52 59 50 54 49 4f 4e 00 12 04 0a d5 8e ENCRYPTION...... + 00000010 c0 1a 04 08 00 12 00 2a 98 06 0a 0c 77 69 64 65 .......*....wide + 00000020 76 69 6e 65 2e 63 6f 6d 12 10 51 43 4f e2 a4 4c vine.com..QCO..L + 00000030 76 3b cc 2c 82 6a 2d 6e f9 a7 1a e0 03 ec 2a c5 v;.,.j-n......*. + */ + }, + onLeave: function (retval) { + print(Level.DEBUG, '[-] onLeave: OEMCrypto_GenerateDerivedKeys'); + } + }); +} + +function OEMCrypto_DeriveKeysFromSessionKey(address) { + /* + wvcdm::OEMCrypto_DeriveKeysFromSessionKey + + Args: + args[0]: OEMCrypto_SESSION session + args[1]: const uint8_t* enc_session_key + args[2]: size_t enc_session_key_length + args[3]: const uint8_t* mac_key_context + args[4]: size_t mac_key_context_length + args[5]: const uint8_t* enc_key_context + args[6]: size_t enc_key_context_length + Return: + retval: OEMCryptoResult + */ + Interceptor.attach(address, { + onEnter: function (args) { + print(Level.DEBUG, '[+] onEnter: OEMCrypto_DeriveKeysFromSessionKey'); + const encSessionKey = Memory.readByteArray(args[1], args[2].toInt32()); + const macKeyContext = Memory.readByteArray(args[3], args[4].toInt32()); + const encKeyContext = Memory.readByteArray(args[5], args[6].toInt32()); + + // Used for L1 license request + // console.log('encSessionKey:', encSessionKey); + /* + encSessionKey: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF + 00000000 c4 9d ef cf 5f 0b 98 9c 03 46 93 89 14 8f 08 e2 ...._....F...... + 00000010 12 da 13 39 ad 31 75 f7 b5 32 94 ee 2f 7f bf 6a ...9.1u..2../..j + 00000020 d7 45 c0 50 22 9a 6c 36 76 a7 78 d8 9f 76 b5 45 .E.P".l6v.x..v.E + 00000030 f3 5c 6f 25 91 08 cf de a3 d9 90 08 cb e1 d4 55 .\o%...........U + */ + + // console.log('macKeyContext:', macKeyContext); + /* + macKeyContext: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF + 00000000 41 55 54 48 45 4e 54 49 43 41 54 49 4f 4e 00 0a AUTHENTICATION.. + 00000010 c3 0f 08 01 12 aa 0b 0a ed 03 08 02 12 20 8d a4 ............. .. + 00000020 21 77 04 fb 58 ff d6 58 80 8c d2 32 b5 81 01 5a !w..X..X...2...Z + 00000030 6a d6 97 29 97 51 ac 92 95 de 81 fe b3 13 18 f7 j..).Q.......... + */ + // console.log('encKeyContext:', encKeyContext); + /* + encKeyContext: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF + 00000000 45 4e 43 52 59 50 54 49 4f 4e 00 0a c3 0f 08 01 ENCRYPTION...... + 00000010 12 aa 0b 0a ed 03 08 02 12 20 8d a4 21 77 04 fb ......... ..!w.. + 00000020 58 ff d6 58 80 8c d2 32 b5 81 01 5a 6a d6 97 29 X..X...2...Zj..) + 00000030 97 51 ac 92 95 de 81 fe b3 13 18 f7 f6 ad c1 06 .Q.............. + */ + }, + onLeave: function (retval) { + print(Level.DEBUG, '[-] onLeave: OEMCrypto_DeriveKeysFromSessionKey'); + } + }); +} + +function WVDrmPlugin_provideProvisionResponse(address) { + /* + wvdrm::WVDrmPlugin::provideProvisionResponse + + Args: + args[1]: const Vector& response + args[2]: Vector& certificate, + args[3]: Vector& wrapped_key + Return: + retval: wvcdm::CdmResponseType + */ + Interceptor.attach(address, { + onEnter: function (args) { + print(Level.DEBUG, '[*] WVDrmPlugin::provideProvisionResponse'); + let dumped = false; + + // Extract and dump the relevant arguments + for (let i = 0; i < 4; i++) { + try { + const responseData = Memory.readStdVector(args[i]); + if (responseData) { + dumped = true; + send('provisioning_response', responseData); + } + } catch (e) { + // print(Level.WARNING, `Failed to extract provisioning response data from args[${i}]`); + } + } + !dumped && print(Level.ERROR, 'Failed to dump provisioning response data'); + } + }); +} + +function Level3_RewrapDeviceRSAKey30(address, name) { + /* + wvcdm::Level3_RewrapDeviceRSAKey30 + + Args: + args[1]: OEMCrypto_SESSION session + args[2]: const uint32_t* nonce + args[3]: const uint8_t* encrypted_message_key + args[4]: size_t encrypted_message_key_length + args[5]: const uint8_t* enc_rsa_key + args[6]: size_t enc_rsa_key_length + args[7]: const uint8_t* enc_rsa_key_iv + args[8]: uint8_t* wrapped_rsa_key + args[9]: size_t* wrapped_rsa_key_length + Return: + retval: OEMCryptoResult + */ + Interceptor.attach(address, { + onEnter: function (args) { + const bufferPtr = args[5]; + const sizePtr = args[6]; + + if (!(sizePtr.isNull() || bufferPtr.isNull())) { + // Check if this is a pointer to a buffer + if (bufferPtr < 0x10000) return; + + // Check if the size matches a potential 2048 or 4096-bit RSA private key (range 1190-2350) + const size = sizePtr.toInt32(); + if (size < 1000 || size > 2400) return; + + const bufferData = readDerKey(bufferPtr, size); + if (bufferData) { + print(Level.DEBUG, `[*] Level3_RewrapDeviceRSAKey30: ${name}`); + send({'private_key': name}, bufferData); + } + } } }); } @@ -401,12 +734,12 @@ const hookLibrary = (name, dynamic) => { if (!library) return false; let functions; - if (SYMBOLS.length) { + if (Object.keys(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) + functions = Object.entries(SYMBOLS).map(([key, value]) => ({ + type: 'function', + name: value, + address: library.base.add(ptr(key)) })); } else { // https://github.com/hyugogirubato/KeyDive/issues/50 @@ -414,55 +747,89 @@ const hookLibrary = (name, dynamic) => { } functions = functions.filter(f => !NATIVE_C_API.includes(f.name)); - let targets = SKIP ? [] : functions.filter(f => OEM_CRYPTO_API.includes(f.name)).map(f => f.name); - const hooked = []; + let targets = DETECT ? functions.filter(f => OEM_CRYPTO_API.includes(f.name)).map(f => f.name) : []; - functions.forEach(func => { - let required = false; - const {name: funcName, address: funcAddr} = func; - if (func.type !== 'function' || hooked.includes(funcAddr)) return; + const required = new Set(); + const hooked = new Set(); + functions.forEach(({name: funcName, address: funcAddr, type}) => { + if (type !== 'function' || required.has(funcAddr)) return; try { - if (funcName.includes('UsePrivacyMode')) { - UsePrivacyMode(funcAddr); - required = true; - } else if (funcName.includes('GetCdmClientPropertySet')) { - GetCdmClientPropertySet(funcAddr); - required = true; - } else if (funcName.includes('PrepareKeyRequest')) { - PrepareKeyRequest(funcAddr); - required = true; - } else if (targets.includes(funcName) || (!targets.length && funcName.match(/^[a-z]+$/))) { - LoadDeviceRSAKey(funcAddr, funcName); - required = true; - } else if (['lcc07', 'oecc07', 'getOemcryptoDeviceId'].some(n => funcName.includes(n))) { - GetDeviceId(funcAddr, funcName); + // Interception of client ID via challenge or in clear text + if (['AesCbcKey', 'Encrypt'].every(n => funcName.includes(n))) { + AesCbcKey_Encrypt(funcAddr); + required.add(funcAddr); + // Calling this method is further down in the challenge request execution flow + // Using the GenerateKeyRequest function at a higher level + //} else if (['CdmLicense', 'PrepareKeyRequest'].every(n => funcName.includes(n))) { + // CdmLicense_PrepareKeyRequest(funcAddr); + // required.add(funcAddr); + } else if (['CdmEngine', 'GenerateKeyRequest'].every(n => funcName.includes(n))) { + CdmEngine_GenerateKeyRequest(funcAddr); + required.add(funcAddr); + + // Full and block keybox interception } else if (['FileSystem', 'Read'].every(n => funcName.includes(n))) { - FileSystemRead(funcAddr); - } else if (['File', 'Read'].every(n => funcName.includes(n)) || funcName.includes('x1c36')) { - FileRead(funcAddr, funcName); - } else if (funcName.includes('runningcrc')) { + FileSystem_Read(funcAddr); + } else if (['File', 'Read'].every(n => funcName.includes(n)) || funcName.includes('_x1c36')) { + File_Read(funcAddr); + } else if (['runningcrc'].every(n => funcName.includes(n))) { // https://github.com/Avalonswanderer/widevinel3_Android_PoC/blob/main/PoCs/recover_l3keybox.py#L50 RunningCRC(funcAddr); + } else if (['_oecc07', '_lcc07'].some(n => funcName.includes(n))) { + OEMCrypto_GetDeviceID(funcAddr); + } else if (['_oecc04', '_lcc04'].some(n => funcName.includes(n))) { + OEMCrypto_GetKeyData(funcAddr); + // TODO: Check the keybox implementation on SDK 36 + // TODO: Interception of the certificate's private key + // Call OEMCrypto_GetOEMPublicCertificate before OEMCrypto_LoadDRMPrivateKey + + // Provisioning Interception + } else if (['_oecc49', '_lcc49'].some(n => funcName.includes(n))) { + OEMCrypto_ProvisioningMethod(funcAddr); + // Key derivation via keybox for L1 provisioning + //} else if (['_oecc12', '_lcc12', '_oecc95', '_lcc95'].some(n => funcName.includes(n))) { + // OEMCrypto_GenerateDerivedKeys(funcAddr); + // Key derivation via session key for L1 license request + //} else if (['_oecc21', '_lcc21'].some(n => funcName.includes(n))) { + // OEMCrypto_DeriveKeysFromSessionKey(funcAddr); + } else if (['WVDrmPlugin', 'provideProvisionResponse'].every(n => funcName.includes(n))) { + WVDrmPlugin_provideProvisionResponse(funcAddr); + + // Disable encrypted client id for license request (deprecated) + } else if (UNENCRYPT && ['Properties', 'GetCdmClientPropertySet'].every(n => funcName.includes(n))) { + Properties_GetCdmClientPropertySet(funcAddr); + } else if (UNENCRYPT && ['Properties', 'UsePrivacyMode'].every(n => funcName.includes(n))) { + // Calling this function usually returns a boolean handled by the GetCdmClientPropertySet subcall + // Replacing this function is quite unstable, causing library crashes + Properties_UsePrivacyMode(funcAddr); + + // OEM private key interruption from obfuscated functions + } else if (targets.includes(funcName) || (!targets.length && funcName.match(/^[a-z]+$/))) { + Level3_RewrapDeviceRSAKey30(funcAddr, funcName); + required.add(funcAddr); } else { return; } - required && hooked.push(funcAddr); + hooked.add(funcAddr); print(Level.DEBUG, `Hooked (${funcAddr}): ${funcName}`); } catch (e) { print(Level.ERROR, `${e.message} for ${funcName}`); } }); - if (hooked.length < 3) { + if (required.size < 3) { print(Level.CRITICAL, 'Insufficient functions hooked'); return false; } - // TODO: Disable old L1 libraries? (https://github.com/wvdumper/dumper/blob/main/Helpers/Scanner.py#L23) - // https://github.com/hzy132/liboemcryptodisabler/blob/master/customize.sh#L33 - disableLibrary('liboemcrypto.so'); + if (DISABLER) { + // TODO: Disable old L1 libraries? (https://github.com/wvdumper/dumper/blob/main/Helpers/Scanner.py#L23) + // https://github.com/hzy132/liboemcryptodisabler/blob/master/customize.sh#L33 + disableLibrary('liboemcrypto.so'); + } + return true; } diff --git a/keydive/utils.py b/keydive/utils.py new file mode 100644 index 0000000..d7705ba --- /dev/null +++ b/keydive/utils.py @@ -0,0 +1,213 @@ +import base64 +import json +import logging + +from datetime import datetime +from pathlib import Path +from typing import Union, Optional +from uuid import UUID + +import coloredlogs +import xmltodict + +from unidecode import unidecode + + +def unidec(data: str) -> str: + """ + Normalizes a string by removing diacritics and accents using ASCII transliteration. + + Args: + data (str): The input string to normalize. + + Returns: + str: The transliterated and stripped version of the input string. + """ + return unidecode(data).strip() + + +def b64enc(data: Union[bytes, str], safe: bool = False) -> str: + """ + Encodes data to Base64 format. + + Args: + data (bytes or str): The data to encode. + safe (bool): Use URL-safe encoding if True. + + Returns: + str: Base64-encoded string. + """ + if isinstance(data, str): + data = data.encode('utf-8') + return (base64.urlsafe_b64encode(data) if safe else base64.b64encode(data)).decode('utf-8') + + +def b64dec(data: str, safe: bool = False) -> bytes: + """ + Decodes Base64-encoded string to bytes. + + Args: + data (str): Base64-encoded string. + safe (bool): Use URL-safe decoding if True. + + Returns: + bytes: Decoded byte data. + """ + # Fix missing padding + data = data + '=' * (-len(data) % 4) + return base64.urlsafe_b64decode(data) if safe else base64.b64decode(data) + + +def xmldec(data: Union[bytes, str], force_list: Optional[list] = None) -> dict: + """ + Parses XML string or bytes into a dictionary. + + Args: + data (str or bytes): The XML data to parse. + force_list (list, optional): Tags to always parse as lists. + + Returns: + dict: Parsed XML structure. + """ + return xmltodict.parse(data, force_list=set(force_list or [])) + + +def dumps(data: Union[dict, list], beauty: bool = False) -> str: + """ + Serializes a Python object into JSON, handling binary, nested, and custom types. + + Args: + data (dict or list): The data to serialize. + beauty (bool): If True, formats JSON with indentation for readability. + + Returns: + str: A JSON-formatted string. + """ + + def __string(value): + # Recursively process lists + if isinstance(value, list): + return [__string(v) for v in value] + + # Recursively process dictionaries + elif isinstance(value, dict): + return {__string(k): __string(v) for k, v in value.items()} + + # Handle byte values (e.g., raw data, encoded JSON, binary keys) + elif isinstance(value, bytes): + if value: + # Try decoding as JSON + try: + return __string(json.loads(value)) + except ValueError: + pass + + # Try decoding as UTF-8 string + try: + return value.decode('utf-8') + except UnicodeDecodeError: + pass + + # Handle specific known binary formats (e.g., 16-byte AES keys) + if len(value) == 16: + return value.hex() + + # Fallback: encode to base64 string for general binary data + return b64enc(value) + + # Return None for empty bytes + return None + + # Recurse again for nested structures + elif isinstance(value, (dict, list)): + return __string(value) + + # Convert known non-serializable types to string + elif isinstance(value, (UUID, datetime, Path)): + return str(value) + + # If value is a string, try parsing it as JSON + elif isinstance(value, str): + try: + return __string(json.loads(value)) + except ValueError: + pass + + # Return all other values unchanged + return value + + # Perform final JSON serialization with optional pretty-printing + return json.dumps( + __string(data), + indent=2 if beauty else None, + separators=None if beauty else (',', ':') # Compact output if not pretty + ) + + +def configure_logging(path: Optional[Path] = None, verbose: bool = False) -> Optional[Path]: + """ + Configures logging to file and console with optional verbosity. + + Args: + path (Path, optional): Directory to save the log file. + verbose (bool): Enable debug-level logging if True. + + Returns: + Path or None: Path to log file if created, otherwise None. + """ + # Set up the root logger with the desired logging level + 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: + # Ensure the log directory exists + 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) + + # Set log formatting + 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 + # 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white' + 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, + field_styles={ + 'asctime': {'color': 'green'}, # timestamp + 'hostname': {'color': 'magenta'}, + 'levelname': {'bold': True, 'color': 'blue'}, # level e.g., [I] + 'name': {'color': 'magenta'}, # logger name + 'programname': {'color': 'cyan'}, + 'message': {'color': 'white'} # message text + }, + level_styles={ + 'debug': {'color': 'cyan'}, + 'info': {'color': 'white'}, + 'warning': {'color': 'yellow'}, + 'error': {'color': 'red'}, + 'critical': {'bold': True, 'color': 'red'} + } + ) + + return file_path diff --git a/keydive/vendor.py b/keydive/vendor.py deleted file mode 100644 index 06af8d2..0000000 --- a/keydive/vendor.py +++ /dev/null @@ -1,34 +0,0 @@ -class Vendor: - """ - Represents a Vendor with SDK, OEM, version, and name attributes. - """ - - def __init__(self, sdk: int, oem: int, version: str, pattern: str): - """ - Initializes a Vendor instance. - - Parameters: - sdk (int): Minimum SDK version required by the vendor. - oem (int): OEM identifier for the vendor. - version (str): Version of the vendor. - pattern (str): Name pattern of the vendor. - """ - self.sdk = sdk - self.oem = oem - self.version = version - self.pattern = pattern - - def __repr__(self) -> str: - """ - Returns a string representation of the Vendor instance. - - Returns: - str: String representation of the Vendor instance with its attributes. - """ - 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/pyproject.toml b/pyproject.toml index 0139f53..e5aa965 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "keydive" -version = "2.2.1" +version = "3.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>"] @@ -34,16 +34,27 @@ include = [ "Changelog" = "https://github.com/hyugogirubato/KeyDive/blob/main/CHANGELOG.md" [tool.poetry.dependencies] -python = "^3.8" -coloredlogs = "^15.0.1" -frida = "^16.6.0" -pathlib = "^1.0.1" -pycryptodomex = "^3.21.0" -pywidevine = "^1.8.0" -pathvalidate = "^3.2.1" -requests = "^2.32.3" -xmltodict = "^0.14.2" -Flask = { version = "^3.0.3", optional = true } +python = ">=3.8,<4.0" +asn1crypto = ">=1.5.1" +coloredlogs = ">=15.0.1" +construct = "^2.10.70" +crccheck = ">=1.3.0" +cryptography = ">=45.0.3" +frida = ">=17.1.3" +pathvalidate = ">=3.2.1" +protobuf = "^5.29.5" +requests = ">=2.32.3" +rich-argparse = ">=1.7.1" +Unidecode = ">=1.4.0" +xmltodict = ">=0.14.2" +Flask = { version = ">=3.0.3", optional = true } + +[tool.poetry.group.dev.dependencies] +frida-tools = ">=14.1.1" +mypy-protobuf = ">=3.6.0" + +[tool.poetry.extras] +offline = ["Flask"] [tool.poetry.scripts] keydive = "keydive.__main__:main" @@ -54,6 +65,4 @@ url = "https://pypi.org/simple/" priority = "primary" [certificates] -localpypi = { cert = false } - - +localpypi = { cert = false } \ No newline at end of file