KeyDive/keydive/__main__.py

207 lines
8.6 KiB
Python
Raw Permalink Normal View History

2024-07-06 18:01:47 +00:00
import argparse
import logging
import time
2024-07-17 22:09:04 +00:00
2024-07-06 18:01:47 +00:00
from datetime import datetime
from pathlib import Path
import coloredlogs
import keydive
2024-07-17 22:09:04 +00:00
2024-10-26 13:21:06 +00:00
from keydive.adb import ADB
2024-07-06 18:01:47 +00:00
from keydive.cdm import Cdm
2024-10-26 13:21:06 +00:00
from keydive.constants import CDM_VENDOR_API, DRM_PLAYER
2024-07-06 18:01:47 +00:00
from keydive.core import Core
2024-10-26 13:21:06 +00:00
def configure_logging(path: Path = None, verbose: bool = False) -> Path:
2024-07-06 18:01:47 +00:00
"""
Configures logging for the application.
Args:
2024-10-26 13:21:06 +00:00
path (Path, optional): The directory to store log files.
verbose (bool, optional): Flag to enable detailed debug logging.
2024-07-06 18:01:47 +00:00
Returns:
Path: The path of log file.
"""
2024-10-26 13:21:06 +00:00
# Set up the root logger with the desired logging level
2024-07-06 18:01:47 +00:00
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:
2024-10-26 13:21:06 +00:00
# Ensure the log directory exists
2024-07-06 18:01:47 +00:00
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)
2024-10-26 13:21:06 +00:00
# Set log formatting
2024-07-06 18:01:47 +00:00
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:
2024-10-26 13:21:06 +00:00
"""
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.
"""
2024-07-06 18:01:47 +00:00
parser = argparse.ArgumentParser(description='Extract Widevine L3 keys from an Android device.')
2024-10-26 13:21:06 +00:00
# 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.')
2024-10-27 18:38:24 +00:00
group_advanced.add_argument('-k', '--keybox', required=False, action='store_true', help='Enable export of the Keybox data if it is available.')
2024-10-26 13:21:06 +00:00
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.')
2024-07-06 18:01:47 +00:00
args = parser.parse_args()
if args.version:
print(f'KeyDive {keydive.__version__}')
exit(0)
# Configure logging
log_path = configure_logging(path=args.log, verbose=args.verbose)
logger = logging.getLogger('KeyDive')
logger.info('Version: %s', keydive.__version__)
try:
2024-10-26 13:21:06 +00:00
# Connect to the specified Android device
adb = ADB(device=args.device)
2024-07-06 18:01:47 +00:00
# Initialize Cdm instance
2024-10-27 18:38:24 +00:00
cdm = Cdm(keybox=args.keybox)
2024-07-06 18:01:47 +00:00
if args.challenge:
cdm.set_challenge(data=args.challenge)
2024-10-26 13:21:06 +00:00
if args.private_key:
cdm.set_private_key(data=args.private_key, name=None)
2024-07-06 18:01:47 +00:00
# Initialize Core instance for interacting with the device
2024-10-26 13:21:06 +00:00
core = Core(adb=adb, cdm=cdm, functions=args.functions, skip=args.skip)
2024-07-06 18:01:47 +00:00
2024-11-01 17:09:09 +00:00
# Setup actions based on user arguments
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')
2024-07-06 18:01:47 +00:00
# Process watcher loop
logger.info('Watcher delay: %ss' % args.delay)
2024-10-26 13:21:06 +00:00
current = None # Variable to track the current Widevine process
2024-07-06 18:01:47 +00:00
while core.running:
2024-07-22 18:31:01 +00:00
# Check if for current process data has been exported
if current and cdm.export(args.output, args.wvd):
2024-10-26 13:21:06 +00:00
raise KeyboardInterrupt # Stop if export is complete
2024-07-22 18:31:01 +00:00
2024-07-06 18:01:47 +00:00
# https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792
2024-10-26 13:21:06 +00:00
# Get the currently running Widevine processes
2024-07-06 18:01:47 +00:00
processes = {
key: (name, pid)
2024-10-26 13:21:06 +00:00
for name, pid in adb.enumerate_processes().items()
2024-07-27 13:41:57 +00:00
for key in CDM_VENDOR_API.keys()
if key in name or key.replace('-service', '-service-lazy') in name
2024-07-06 18:01:47 +00:00
}
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
2024-07-22 18:31:01 +00:00
if current and current not in [v[1] for v in processes.values()]:
logger.warning('Widevine process has changed')
current = None
2024-07-06 18:01:47 +00:00
# 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
2024-07-08 16:25:33 +00:00
for vendor in CDM_VENDOR_API[key]:
2024-07-06 18:01:47 +00:00
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:
2024-07-08 17:01:33 +00:00
logger.info('Successfully hooked')
2024-07-06 18:01:47 +00:00
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()