2026-04-24 06:01:21 +02:00
|
|
|
# pywv
|
2026-04-24 06:06:50 +02:00
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
**pywv** is a Python-based tool for parsing, analyzing, and interacting with **Widevine (WVD) device files** and DRM structures.
|
|
|
|
|
It provides direct access to device data, cryptographic components, and license workflows for controlled inspection and processing.
|
2026-04-24 06:06:50 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Features
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
* Parsing and loading of **Widevine Device (WVD) files**
|
|
|
|
|
* Support for **WVD version migration (v1 → v2)**
|
|
|
|
|
* Device loading from:
|
|
|
|
|
|
|
|
|
|
* `.wvd` files
|
|
|
|
|
* key + client_id blobs
|
|
|
|
|
* device directories
|
|
|
|
|
* Extraction of **device attributes** (system ID, security level)
|
|
|
|
|
* Decoding of **Widevine protobuf structures**
|
|
|
|
|
* Integration with **cryptographic primitives** (RSA, AES, CMAC, HMAC)
|
|
|
|
|
* Parsing and manipulation of **Widevine PSSH**
|
|
|
|
|
* License processing and **content key extraction**
|
|
|
|
|
* Optional **VMP (Verified Media Path) support**
|
|
|
|
|
* Built-in CDM logic (no external binaries required)
|
2026-04-24 06:06:50 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Requirements
|
|
|
|
|
|
|
|
|
|
* Python 3.9 or higher
|
|
|
|
|
* `pycryptodome`
|
|
|
|
|
* `construct`
|
|
|
|
|
* `protobuf`
|
|
|
|
|
* `pymp4`
|
|
|
|
|
* `requests`
|
|
|
|
|
|
|
|
|
|
Installation:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
pip install pycryptodome construct protobuf pymp4 requests
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Usage
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-04-30 05:53:23 +02:00
|
|
|
python pywv.py <command> [options]
|
2026-04-24 06:06:50 +02:00
|
|
|
```
|
2026-04-30 05:53:23 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Commands
|
|
|
|
|
|
|
|
|
|
### info
|
|
|
|
|
|
|
|
|
|
Inspect a device:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py info --wvd device.wvd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Alternative input modes:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py info --device-dir path/
|
|
|
|
|
python pywv.py info --key device_private_key --client-id device_client_id_blob
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Optional:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
--vmp device_vmp_blob
|
|
|
|
|
```
|
|
|
|
|
|
2026-04-24 06:06:50 +02:00
|
|
|
---
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
### license
|
|
|
|
|
|
|
|
|
|
Execute the full license workflow (challenge generation, request submission, response parsing, key extraction):
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py license \
|
|
|
|
|
--wvd device.wvd \
|
|
|
|
|
--pssh BASE64_PSSH \
|
|
|
|
|
--url LICENSE_URL
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Using raw blobs:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py license \
|
|
|
|
|
--key device_private_key \
|
|
|
|
|
--client-id device_client_id_blob \
|
|
|
|
|
--pssh BASE64_PSSH \
|
|
|
|
|
--url LICENSE_URL
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Challenge-only mode:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py license \
|
|
|
|
|
--wvd device.wvd \
|
|
|
|
|
--pssh BASE64_PSSH
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### create-wvd
|
|
|
|
|
|
|
|
|
|
Create a WVD file from raw device components:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py create-wvd \
|
|
|
|
|
--key device_private_key \
|
|
|
|
|
--client-id device_client_id_blob \
|
|
|
|
|
-o device.wvd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
With VMP:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
--vmp device_vmp_blob
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### export-wvd
|
|
|
|
|
|
|
|
|
|
Export a WVD file into raw device components:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py export-wvd \
|
|
|
|
|
--wvd device.wvd \
|
|
|
|
|
-o output_dir
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### migrate-wvd
|
|
|
|
|
|
|
|
|
|
Migrate a WVD v1 file to WVD v2:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py migrate-wvd \
|
|
|
|
|
-i old.wvd \
|
|
|
|
|
-o new.wvd
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### pssh
|
|
|
|
|
|
|
|
|
|
Inspect PSSH data:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py pssh --pssh BASE64_PSSH
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Rewrite key IDs:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python pywv.py pssh --pssh BASE64_PSSH --set-kid NEW_KID
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Python Usage
|
2026-04-24 06:06:50 +02:00
|
|
|
|
|
|
|
|
### Basic Usage
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
import requests
|
|
|
|
|
from pywv import PSSH, Device, Cdm
|
|
|
|
|
|
|
|
|
|
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
device = Device.load("device.wvd")
|
|
|
|
|
|
2026-04-24 06:06:50 +02:00
|
|
|
cdm = Cdm.from_device(device)
|
|
|
|
|
session_id = cdm.open()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
challenge = cdm.get_license_challenge(session_id, pssh)
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
response = requests.post(
|
2026-04-24 06:06:50 +02:00
|
|
|
"https://cwip-shaka-proxy.appspot.com/no_auth",
|
|
|
|
|
data=challenge,
|
|
|
|
|
)
|
2026-04-30 05:53:23 +02:00
|
|
|
response.raise_for_status()
|
2026-04-24 06:06:50 +02:00
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
cdm.parse_license(session_id, response.content)
|
2026-04-24 06:06:50 +02:00
|
|
|
|
|
|
|
|
for key in cdm.get_keys(session_id):
|
|
|
|
|
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
cdm.close(session_id)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
### Using Key / Certificate (no JSON)
|
2026-04-24 06:06:50 +02:00
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
import requests
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from pywv import PSSH, Device, Cdm
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
base = Path(__file__).resolve().parent
|
2026-04-24 06:06:50 +02:00
|
|
|
|
|
|
|
|
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
device = Device.from_files(
|
|
|
|
|
certificate=base / "device_client_id_blob",
|
|
|
|
|
key=base / "device_private_key",
|
|
|
|
|
vmp=False
|
|
|
|
|
)
|
2026-04-24 06:06:50 +02:00
|
|
|
|
|
|
|
|
cdm = Cdm.from_device(device)
|
|
|
|
|
session_id = cdm.open()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
challenge = cdm.get_license_challenge(session_id, pssh)
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
response = requests.post(
|
2026-04-24 06:06:50 +02:00
|
|
|
"https://cwip-shaka-proxy.appspot.com/no_auth",
|
|
|
|
|
data=challenge,
|
|
|
|
|
)
|
2026-04-30 05:53:23 +02:00
|
|
|
response.raise_for_status()
|
2026-04-24 06:06:50 +02:00
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
cdm.parse_license(session_id, response.content)
|
2026-04-24 06:06:50 +02:00
|
|
|
|
|
|
|
|
for key in cdm.get_keys(session_id):
|
|
|
|
|
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
|
|
|
|
|
|
|
|
|
|
finally:
|
|
|
|
|
cdm.close(session_id)
|
|
|
|
|
```
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
### Create WVD
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from pywv import Device
|
|
|
|
|
|
|
|
|
|
device = Device.from_files(
|
|
|
|
|
certificate="device_client_id_blob",
|
|
|
|
|
key="device_private_key",
|
|
|
|
|
vmp=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
device.dump("device.wvd")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Export WVD
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from pywv import Device
|
|
|
|
|
|
|
|
|
|
out = Path("exported")
|
|
|
|
|
out.mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
device = Device.load("device.wvd")
|
|
|
|
|
|
|
|
|
|
(out / "device_private_key").write_bytes(
|
|
|
|
|
device.private_key.export_key("DER")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
(out / "device_client_id_blob").write_bytes(
|
|
|
|
|
device.client_id.SerializeToString()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if device.client_id.vmp_data:
|
|
|
|
|
(out / "device_vmp_blob").write_bytes(
|
|
|
|
|
device.client_id.vmp_data
|
|
|
|
|
)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### Migrate WVD v1 → v2
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from pywv import Device
|
|
|
|
|
|
|
|
|
|
with open("old.wvd", "rb") as f:
|
|
|
|
|
data = f.read()
|
|
|
|
|
|
|
|
|
|
device = Device.migrate(data)
|
|
|
|
|
device.dump("new.wvd")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
### PSSH
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from pywv import PSSH
|
|
|
|
|
|
|
|
|
|
pssh = PSSH("BASE64")
|
|
|
|
|
|
|
|
|
|
print(pssh.key_ids)
|
|
|
|
|
|
|
|
|
|
new_pssh = PSSH.new(
|
|
|
|
|
system_id=PSSH.SystemId.Widevine,
|
|
|
|
|
key_ids=pssh.key_ids,
|
|
|
|
|
version=1
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
print(new_pssh.dumps())
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Output
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
[CONTENT] kid:key
|
|
|
|
|
```
|
|
|
|
|
|
2026-04-24 06:06:50 +02:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Disclaimer
|
|
|
|
|
|
|
|
|
|
This tool is intended strictly for **educational, research, and interoperability purposes**.
|
2026-04-30 05:53:23 +02:00
|
|
|
The author does not support or encourage misuse of DRM systems or unauthorized access to protected content.
|
2026-04-24 06:06:50 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Issues and Support
|
|
|
|
|
|
|
|
|
|
For bug reports, feature requests, or general inquiries, please open an issue in the repository.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Acknowledgements
|
|
|
|
|
|
2026-04-30 05:53:23 +02:00
|
|
|
This project is based on **pywidevine**, adapted for standalone usage and improved WVD and PSSH workflows.
|