| name | drm-pentesting-toolkit |
| description | Plugin-based DRM pentesting toolkit with GUI for device management, PSSH extraction, license key retrieval, and content downloading |
| triggers | ["extract DRM keys from streaming content","test Widevine or PlayReady device files","parse PSSH from MPD manifest","download encrypted streaming content","validate DRM device credentials","create DRM pentesting plugin","chain DRM pentesting operations","extract license server keys"] |
DRM Pentesting Toolkit Skill
Skill by ara.so — Security Skills collection.
This skill enables AI agents to assist with DRM pentesting using a modular, plugin-based toolkit. The project provides a CustomTkinter GUI for managing DRM devices (Widevine .wvd, PlayReady .prd), extracting PSSH from MPD manifests, communicating with license servers, and downloading protected content.
What It Does
The DRM Pentesting Toolkit is a comprehensive framework for:
- Loading and validating Widevine/PlayReady device files
- Parsing PSSH (Protection System Specific Header) data from DASH/MPD manifests
- Extracting decryption keys from license servers
- Generating M3U playlists with embedded keys
- Downloading and decrypting content via N_m3u8DL-RE
- Managing credentials, proxies, and HTTP configurations
- Chaining multiple operations through a plugin stack system
Installation
git clone https://github.com/fairy-root/drm-pentesting-toolkit.git
cd drm-pentesting-toolkit
pip install -r requirements.txt
mkdir -p devices/widevine devices/playready credentials proxies N_m3u8DL-RE OUTPUT
Required dependencies (from requirements.txt):
customtkinter>=5.2.0
pywidevine>=1.8.0
pyplayready>=1.3.0
requests>=2.31.0
Brotli>=1.1.0
protobuf>=4.25.0
Additional setup:
- Place DRM device files in
devices/widevine/*.wvd or devices/playready/*.prd
- Download N_m3u8DL-RE and shaka-packager executables to
N_m3u8DL-RE/
- Create credential files in
credentials/ (format: username:password)
- Create proxy files in
proxies/ (format: host:port:user:pass)
Running the Application
python main.py
The GUI provides tabs for:
- MPD Data: Configure manifest URLs, headers, cookies
- License Data: Set license server URLs and authentication
- Logs & Output: Monitor plugin execution
- Downloads: Track N_m3u8DL-RE download progress
Core Architecture
Plugin System
Plugins are Python files in plugins/ that define a run() function. The plugin manager uses introspection to:
- Auto-detect required parameters
- Map UI context to function arguments
- Pass results between chained plugins
Plugin context variables available:
device_path: str
device_type: str
cdm: object
session_id: bytes
pssh: str
license_url: str
mpd_url: str
mpd_headers: dict
mpd_cookies: dict
license_headers: dict
license_cookies: dict
license_post_data: str
keys: list
credentials: dict
proxy_line: str
search_query: str
Creating a Plugin
def run(mpd_url, mpd_headers, log_callback=None):
"""
Custom plugin that fetches and analyzes MPD manifest
Args:
mpd_url: The DASH manifest URL
mpd_headers: Dictionary of HTTP headers
log_callback: Function to log messages to GUI
Returns:
dict: Context updates for next plugin
"""
import requests
if log_callback:
log_callback(f"Fetching MPD from: {mpd_url}")
try:
response = requests.get(mpd_url, headers=mpd_headers, timeout=10)
response.raise_for_status()
mpd_content = response.text
if log_callback:
log_callback(f"MPD size: {len(mpd_content)} bytes")
return {
'mpd_content': mpd_content,
'mpd_size': len(mpd_content)
}
except Exception as e:
if log_callback:
log_callback(f"Error: {str(e)}")
raise
Common Workflows
Key Extraction Workflow
def run(device_path, device_type, log_callback=None):
from pywidevine.cdm import Cdm as WidevineCdm
from pywidevine.device import Device as WidevineDevice
if device_type == 'widevine':
device = WidevineDevice.load(device_path)
cdm = WidevineCdm.from_device(device)
session_id = cdm.open()
if log_callback:
log_callback(f"Widevine session opened: {session_id.hex()}")
return {
'cdm': cdm,
'session_id': session_id,
'device_security_level': device.security_level
}
def run(mpd_url, mpd_headers, mpd_cookies, http_method='GET', log_callback=None):
import requests
import re
from base64 import b64decode
response = requests.request(
method=http_method,
url=mpd_url,
headers=mpd_headers,
cookies=mpd_cookies,
timeout=15
)
pssh_pattern = r'<cenc:pssh>([A-Za-z0-9+/=]+)</cenc:pssh>'
matches = re.findall(pssh_pattern, response.text)
if matches:
pssh = matches[]
log_callback:
log_callback()
{: pssh}
ValueError()
():
requests
base64 b64decode, b64encode
challenge = cdm.get_license_challenge(session_id, b64decode(pssh))
payload = license_post_data.replace(, b64encode(challenge).decode())
response = requests.post(
license_url,
headers=license_headers,
cookies=license_cookies,
data=payload
)
cdm.parse_license(session_id, response.content)
keys = []
key cdm.get_keys(session_id):
key. == :
key_str =
keys.append(key_str)
log_callback:
log_callback()
{: keys}
Download Workflow
def run(mpd_url, keys, search_query='output', log_callback=None):
import subprocess
import os
exe_path = os.path.join('N_m3u8DL-RE', 'N_m3u8DL-RE.exe')
key_args = []
for key in keys:
key_args.extend(['--key', key])
cmd = [
exe_path,
mpd_url,
'--save-dir', 'OUTPUT',
'--save-name', search_query,
'--auto-select',
'--no-log'
] + key_args
if log_callback:
log_callback(f"Starting download: {search_query}")
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
return {
'download_process': process,
'output_name': search_query
}
Credential Integration
def run(credentials, license_url, log_callback=None):
"""
Use credentials to authenticate with license server
credentials structure:
{
'username': 'user@example.com',
'password': 'pass123',
'api_key': 'abc123xyz',
'mac_address': '00:11:22:33:44:55'
}
"""
import requests
if 'api_key' in credentials:
headers = {
'Authorization': f"Bearer {credentials['api_key']}"
}
elif 'username' in credentials and 'password' in credentials:
headers = {
'X-Username': credentials['username'],
'X-Password': credentials['password']
}
else:
raise ValueError("No valid credentials found")
response = requests.post(license_url, headers=headers)
if log_callback:
log_callback(f"Authenticated with {list(credentials.keys())}")
return {'auth_headers': headers}
Configuration
Environment Settings
Edit settings/environment.ini to configure credential types:
[CREDENTIALS]
EMAIL_PASSWORD = ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}:.+$
USERNAME_PASSWORD = ^[a-zA-Z0-9_]+:.+$
API_KEY = ^[a-zA-Z0-9]{20,}$
MAC_ADDRESS = ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$
State Persistence
Application state is saved to settings/savestate.json:
{
"mpd_url": "https://example.com/manifest.mpd",
"mpd_headers": "{\"User-Agent\": \"Mozilla/5.0\"}",
"license_url": "https://license.example.com/widevine",
"selected_device": "devices/widevine/device.wvd",
"selected_credentials": "credentials/account.txt",
"plugin_stack": ["1_load_devices", "2_parse_pssh", "4_send_license"]
}
Proxy Configuration
Format proxy files as proxies/myproxies.txt:
# Non-authenticated
proxy1.example.com:8080
192.168.1.100:3128
# Authenticated
proxy2.example.com:8080:username:password
Access in plugins:
def run(proxy_line, log_callback=None):
if proxy_line:
parts = proxy_line.split(':')
if len(parts) == 2:
proxy = f"http://{parts[0]}:{parts[1]}"
elif len(parts) == 4:
proxy = f"http://{parts[2]}:{parts[3]}@{parts[0]}:{parts[1]}"
proxies = {
'http': proxy,
'https': proxy
}
return {'proxies': proxies}
Programmatic Usage
Using Plugin Manager Directly
from core.plugin_manager import PluginManager
pm = PluginManager(plugins_dir='plugins')
pm.scan_plugins()
context = {
'device_path': 'devices/widevine/l3.wvd',
'device_type': 'widevine',
'mpd_url': 'https://example.com/manifest.mpd',
'mpd_headers': {'User-Agent': 'Mozilla/5.0'},
'license_url': 'https://license.example.com/widevine'
}
plugin_stack = ['1_load_devices', '2_parse_pssh', '4_send_license']
for plugin_name in plugin_stack:
plugin_info = pm.plugins[plugin_name]
result = pm.run_plugin(plugin_info, context, log_callback=print)
if isinstance(result, dict):
context.update(result)
print(f"Extracted keys: {context.get('keys', [])}")
Validating Device Files
from pywidevine.cdm import Cdm
from pywidevine.device import Device
def validate_widevine_device(device_path):
"""Validate a Widevine device file"""
try:
device = Device.load(device_path)
cdm = Cdm.from_device(device)
session_id = cdm.open()
print(f"✓ Valid Widevine device")
print(f" Security Level: {device.security_level}")
print(f" Client ID: {device.client_id.token[:20].hex()}...")
cdm.close(session_id)
return True
except Exception as e:
print(f"✗ Invalid device: {e}")
return False
validate_widevine_device('devices/widevine/device.wvd')
Troubleshooting
Plugin Not Detecting Parameters
Issue: Plugin not receiving expected context variables
Solution: Check function signature matches available context:
def run(manifest_url, log_callback=None):
pass
def run(mpd_url, log_callback=None):
pass
PSSH Extraction Fails
Issue: No PSSH found in MPD manifest
Solution: Check HTTP method and authentication:
response = requests.request(
method='POST',
url=mpd_url,
headers=mpd_headers
)
pssh_patterns = [
r'<cenc:pssh>([^<]+)</cenc:pssh>',
r'cenc:default_KID="([^"]+)"',
r'<widevine:license>([^<]+)</widevine:license>'
]
License Request Fails
Issue: 403/401 errors from license server
Solution: Verify headers and credential handling:
required_headers = {
'User-Agent': 'Mozilla/5.0...',
'Origin': 'https://example.com',
'Referer': 'https://example.com/player',
'Content-Type': 'application/octet-stream'
}
payload_formats = [
b64encode(challenge).decode(),
json.dumps({'challenge': b64encode(challenge).decode()}),
f'spc={b64encode(challenge).decode()}'
]
Download Process Hangs
Issue: N_m3u8DL-RE download not progressing
Solution: Check executable path and permissions:
import os
import stat
exe_path = 'N_m3u8DL-RE/N_m3u8DL-RE.exe'
if not os.path.exists(exe_path):
raise FileNotFoundError(f"N_m3u8DL-RE not found at {exe_path}")
if os.name != 'nt':
st = os.stat(exe_path)
os.chmod(exe_path, st.st_mode | stat.S_IEXEC)
Device Loading Errors
Issue: CDM fails to initialize
Solution: Verify device file format and dependencies:
try:
from pywidevine.device import Device
device = Device.load(device_path)
print(f"Client ID length: {len(device.client_id.token)}")
print(f"Private key present: {device.private_key is not None}")
except Exception as e:
print(f"Device load error: {e}")
Credentials Not Exposing
Issue: credentials parameter not available in plugin
Solution: Ensure "Expose Credentials to Plugins" checkbox is enabled in GUI, and credentials file is properly formatted:
user@example.com:password123
Advanced Patterns
Multi-Key Content
def run(cdm, session_id, mpd_content, log_callback=None):
"""Extract keys for multi-period content"""
import re
from base64 import b64decode
pssh_list = re.findall(r'<cenc:pssh>([^<]+)</cenc:pssh>', mpd_content)
all_keys = []
for i, pssh in enumerate(set(pssh_list)):
try:
challenge = cdm.get_license_challenge(session_id, b64decode(pssh))
keys = cdm.get_keys(session_id)
all_keys.extend(keys)
if log_callback:
log_callback(f"Period {i+1}: {len(keys)} keys")
except Exception as e:
if log_callback:
log_callback(f"Period {i+1} failed: {e}")
return {'keys': all_keys}
Custom Headers from Environment
def run(mpd_url, log_callback=None):
"""Use environment variables for sensitive headers"""
import os
import requests
headers = {
'User-Agent': os.getenv('USER_AGENT', 'Mozilla/5.0'),
'Authorization': os.getenv('AUTH_TOKEN'),
'X-Custom-Header': os.getenv('CUSTOM_HEADER')
}
headers = {k: v for k, v in headers.items() if v is not None}
response = requests.get(mpd_url, headers=headers)
return {'mpd_content': response.text}
This skill enables AI agents to help developers perform DRM security testing, create custom plugins, and automate key extraction workflows using this comprehensive toolkit.