2024-07-06 20:01:47 +02:00
import argparse
import logging
import subprocess
import time
2024-07-18 00:09:04 +02:00
2024-07-06 20:01:47 +02:00
from datetime import datetime
from pathlib import Path
import coloredlogs
import keydive
2024-07-18 00:09:04 +02:00
2024-07-06 20:01:47 +02:00
from keydive.cdm import Cdm
from keydive.constants import CDM_VENDOR_API
from keydive.core import Core
def configure_logging ( path : Path , verbose : bool ) -> Path :
"""
Configures logging for the application.
Args:
path (Path, optional): The path for log files.
verbose (bool): Whether to enable verbose logging.
Returns:
Path: The path of log file.
"""
# Get the root logger
root_logger = logging . getLogger ()
root_logger . setLevel ( logging . DEBUG if verbose else logging . INFO )
# Clear any existing handlers (optional, to avoid duplicate logs if reconfiguring)
if root_logger . hasHandlers ():
root_logger . handlers . clear ()
file_path = None
if path :
if path . is_file ():
path = path . parent
path . mkdir ( parents = True , exist_ok = True )
# Create a file handler
file_path = path / ( 'keydive_ %s .log' % datetime . now () . strftime ( '%Y-%m- %d _%H-%M-%S' ))
file_path = file_path . resolve ( strict = False )
file_handler = logging . FileHandler ( file_path )
file_handler . setLevel ( logging . DEBUG )
formatter = logging . Formatter (
fmt = ' %(asctime)s [ %(levelname).1s ] %(name)s : %(message)s ' ,
datefmt = '%Y-%m- %d %H:%M:%S'
)
file_handler . setFormatter ( formatter )
# Add the file handler to the root logger
root_logger . addHandler ( file_handler )
# Configure coloredlogs for console output
coloredlogs . install (
fmt = ' %(asctime)s [ %(levelname).1s ] %(name)s : %(message)s ' ,
datefmt = '%Y-%m- %d %H:%M:%S' ,
level = logging . DEBUG if verbose else logging . INFO ,
logger = root_logger
)
return file_path
def main () -> None :
parser = argparse . ArgumentParser ( description = 'Extract Widevine L3 keys from an Android device.' )
# Global options
opt_global = parser . add_argument_group ( 'Global options' )
opt_global . add_argument ( '-d' , '--device' , required = False , type = str , metavar = '<id>' , help = 'Specify the target Android device ID to connect with via ADB.' )
opt_global . add_argument ( '-v' , '--verbose' , required = False , action = 'store_true' , help = 'Enable verbose logging for detailed debug output.' )
opt_global . add_argument ( '-l' , '--log' , required = False , type = Path , metavar = '<dir>' , help = 'Directory to store log files.' )
opt_global . add_argument ( '--delay' , required = False , type = float , metavar = '<delay>' , default = 1 , help = 'Delay (in seconds) between process checks in the watcher.' )
opt_global . add_argument ( '--version' , required = False , action = 'store_true' , help = 'Display KeyDive version information.' )
# Cdm options
opt_cdm = parser . add_argument_group ( 'Cdm options' )
opt_cdm . add_argument ( '-a' , '--auto' , required = False , action = 'store_true' , help = 'Automatically open Bitmovin \' s demo.' )
opt_cdm . add_argument ( '-c' , '--challenge' , required = False , type = Path , metavar = '<file>' , help = 'Path to unencrypted challenge for extracting client ID.' )
2024-07-06 21:17:00 +02:00
opt_cdm . add_argument ( '-w' , '--wvd' , required = False , action = 'store_true' , help = 'Generate a pywidevine WVD device file.' )
2024-07-06 20:01:47 +02:00
opt_cdm . add_argument ( '-o' , '--output' , required = False , type = Path , default = Path ( 'device' ), metavar = '<dir>' , help = 'Output directory path for extracted data.' )
opt_cdm . add_argument ( '-f' , '--functions' , required = False , type = Path , metavar = '<file>' , help = 'Path to Ghidra XML functions file.' )
args = parser . parse_args ()
if args . version :
print ( f 'KeyDive { keydive . __version__ } ' )
exit ( 0 )
# Configure logging
log_path = configure_logging ( path = args . log , verbose = args . verbose )
logger = logging . getLogger ( 'KeyDive' )
logger . info ( 'Version: %s ' , keydive . __version__ )
try :
# Start the ADB server if not already running
sp = subprocess . run ([ 'adb' , 'start-server' ], capture_output = True )
if sp . returncode != 0 :
raise EnvironmentError ( 'ADB is not recognized as an environment variable, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge' )
# Initialize Cdm instance
cdm = Cdm ()
if args . challenge :
cdm . set_challenge ( data = args . challenge )
# Initialize Core instance for interacting with the device
2024-07-07 10:22:21 +02:00
core = Core ( cdm = cdm , device = args . device , functions = args . functions )
2024-07-06 20:01:47 +02:00
# Process watcher loop
logger . info ( 'Watcher delay: %s s' % args . delay )
current = None
while core . running :
# https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792
processes = {
key : ( name , pid )
for name , pid in core . enumerate_processes () . items ()
2024-07-08 18:25:33 +02:00
for key in CDM_VENDOR_API . keys () if key in name
2024-07-06 20:01:47 +02: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
if current :
if current not in [ v [ 1 ] for v in processes . values ()]:
logger . warning ( 'Widevine process has changed' )
current = None
elif cdm . export ( args . output , args . wvd ):
raise KeyboardInterrupt
# If current process not found, attempt to hook into the detected processes
if not current :
logger . debug ( 'Analysing...' )
for key , ( name , pid ) in processes . items ():
if current :
break
2024-07-08 18:25:33 +02:00
for vendor in CDM_VENDOR_API [ key ]:
2024-07-06 20:01:47 +02: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 19:01:33 +02:00
logger . info ( 'Successfully hooked' )
2024-07-06 20:01:47 +02:00
if args . auto :
logger . info ( 'Starting DRM player launch process...' )
sp = subprocess . run ([ 'adb' , '-s' , str ( core . device . id ), 'shell' , 'am' , 'start' , '-a' , 'android.intent.action.VIEW' , '-d' , 'https://bitmovin.com/demos/drm' ], capture_output = True )
if sp . returncode != 0 :
logger . error ( 'Error launching DRM player: %s ' % sp . stdout . decode ( 'utf-8' ) . strip ())
else :
logger . warning ( 'Widevine library not found, searching...' )
# Delay before next iteration
time . sleep ( args . delay )
except KeyboardInterrupt :
pass
except Exception as e :
logger . critical ( e , exc_info = args . verbose )
# Final logging and exit
if log_path :
logger . info ( 'Log file: %s ' % log_path )
logger . info ( 'Exiting' )
if __name__ == '__main__' :
main ()