release v3.0.0

This commit is contained in:
hyugogirubato
2025-06-09 18:12:49 +02:00
parent 2f74b4c5b7
commit 82d6a903dd
28 changed files with 5384 additions and 1620 deletions
+60 -173
View File
@@ -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='<serial>', help='ADB serial number of the target Android device.')
global_group.add_argument('-d', '--delay', type=float, metavar='<delay>', 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='<dir>', 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="<id>", 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="<dir>", help="Directory to store log files.")
group_global.add_argument('--delay', required=False, type=float, metavar="<delay>", 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='<dir>', 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='<type>', 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="<dir>", 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="<file>", 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="<file>", help="Path to unencrypted challenge for extracting client ID.")
group_advanced.add_argument('--private-key', required=False, type=Path, metavar="<file>", 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='<symbols>', help='Path to Ghidra-generated XML symbol file for function mapping.')
advanced_group.add_argument('--challenge', action='append', type=Path, metavar='<challenge>', help='Protobuf challenge file(s) captured via MITM proxy.')
advanced_group.add_argument('--rsa-key', action='append', type=Path, metavar='<rsa-key>', 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='<aes-key>', 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()