209 lines
8.8 KiB
Python
209 lines
8.8 KiB
Python
import argparse
|
|
import logging
|
|
import time
|
|
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import coloredlogs
|
|
|
|
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
|
|
|
|
|
|
def main() -> None:
|
|
"""
|
|
Main entry point for the KeyDive application.
|
|
|
|
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 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.")
|
|
|
|
# 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.")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Handle version display
|
|
if args.version:
|
|
print(f"KeyDive {keydive.__version__}")
|
|
exit(0)
|
|
|
|
# Configure logging (file and console)
|
|
log_path = configure_logging(path=args.log, verbose=args.verbose)
|
|
logger = logging.getLogger("KeyDive")
|
|
logger.info("Version: %s", keydive.__version__)
|
|
|
|
try:
|
|
# Connect to the specified Android device
|
|
adb = ADB(device=args.device)
|
|
|
|
# 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)
|
|
|
|
# Initialize Core instance for interacting with the device
|
|
core = Core(adb=adb, cdm=cdm, functions=args.functions, skip=args.skip)
|
|
|
|
# Setup actions based on user arguments (for DRM player, Bitmovin player, etc.)
|
|
if args.player:
|
|
package = DRM_PLAYER["package"]
|
|
|
|
# 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"])
|
|
|
|
# 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)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
except Exception as e:
|
|
logger.critical(e, exc_info=args.verbose)
|
|
|
|
# Final logging and exit
|
|
if log_path:
|
|
logger.info("Log file: %s" % log_path)
|
|
logger.info("Exiting")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|