mirror of
https://github.com/Hugoved/pywv.git
synced 2026-07-15 18:10:06 +02:00
Update README.md
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# pywv
|
# pywv
|
||||||
|
|
||||||
**pywv** is a Python-based tool for parsing, analyzing, and interacting with **Widevine (WVD) device files** and associated DRM structures. It provides low-level access to device data, cryptographic components, and license-related metadata, enabling precise inspection and controlled handling of Widevine workflows.
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -8,11 +9,18 @@
|
|||||||
|
|
||||||
* Parsing and loading of **Widevine Device (WVD) files**
|
* Parsing and loading of **Widevine Device (WVD) files**
|
||||||
* Support for **WVD version migration (v1 → v2)**
|
* Support for **WVD version migration (v1 → v2)**
|
||||||
* Extraction of **device attributes** (system ID, security level, client identification)
|
* Device loading from:
|
||||||
* Decoding of **Widevine protobuf structures** (ClientIdentification, certificates, licenses)
|
|
||||||
|
* `.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)
|
* Integration with **cryptographic primitives** (RSA, AES, CMAC, HMAC)
|
||||||
* Parsing and manipulation of **PSSH (Widevine / PlayReady)** containers
|
* Parsing and manipulation of **Widevine PSSH**
|
||||||
* License processing and **content key extraction** via CDM workflows
|
* License processing and **content key extraction**
|
||||||
|
* Optional **VMP (Verified Media Path) support**
|
||||||
|
* Built-in CDM logic (no external binaries required)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -36,11 +44,127 @@ pip install pycryptodome construct protobuf pymp4 requests
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python pywv.py [options]
|
python pywv.py <command> [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Examples
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
@@ -50,20 +174,21 @@ from pywv import PSSH, Device, Cdm
|
|||||||
|
|
||||||
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
||||||
|
|
||||||
device = Device.load("xiaomi_redmi_note_4x_15.0.0_e15a9e5f_8159_l3.wvd")
|
device = Device.load("device.wvd")
|
||||||
|
|
||||||
cdm = Cdm.from_device(device)
|
cdm = Cdm.from_device(device)
|
||||||
session_id = cdm.open()
|
session_id = cdm.open()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
challenge = cdm.get_license_challenge(session_id, pssh)
|
challenge = cdm.get_license_challenge(session_id, pssh)
|
||||||
|
|
||||||
license_response = requests.post(
|
response = requests.post(
|
||||||
"https://cwip-shaka-proxy.appspot.com/no_auth",
|
"https://cwip-shaka-proxy.appspot.com/no_auth",
|
||||||
data=challenge,
|
data=challenge,
|
||||||
)
|
)
|
||||||
license_response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
cdm.parse_license(session_id, license_response.content)
|
cdm.parse_license(session_id, response.content)
|
||||||
|
|
||||||
for key in cdm.get_keys(session_id):
|
for key in cdm.get_keys(session_id):
|
||||||
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
|
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
|
||||||
@@ -74,44 +199,22 @@ finally:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Using JSON Device Configuration
|
### Using Key / Certificate (no JSON)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# wv.json example:
|
|
||||||
# {
|
|
||||||
# "security_level": 3,
|
|
||||||
# "session_id_type": "android",
|
|
||||||
# "private_key_available": true,
|
|
||||||
# "vmp": false
|
|
||||||
# }
|
|
||||||
|
|
||||||
import json
|
|
||||||
import requests
|
import requests
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pywv import PSSH, Device, Cdm
|
from pywv import PSSH, Device, Cdm
|
||||||
|
|
||||||
base_path = Path(__file__).resolve().parent
|
base = Path(__file__).resolve().parent
|
||||||
|
|
||||||
with open(base_path / "wv.json", "r", encoding="utf-8") as f:
|
|
||||||
wv = json.load(f)
|
|
||||||
|
|
||||||
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
||||||
|
|
||||||
if not wv.get("private_key_available", True):
|
device = Device.from_files(
|
||||||
raise ValueError("This device profile does not include a private key")
|
certificate=base / "device_client_id_blob",
|
||||||
|
key=base / "device_private_key",
|
||||||
device_kwargs = {
|
vmp=False
|
||||||
"private_key": base_path / "device_private_key",
|
)
|
||||||
"client_id": base_path / "device_client_id_blob",
|
|
||||||
"type_": wv.get("session_id_type", "ANDROID").upper(),
|
|
||||||
"security_level": wv.get("security_level", 3),
|
|
||||||
}
|
|
||||||
|
|
||||||
vmp_path = base_path / "device_vmp_blob"
|
|
||||||
if wv.get("vmp", False) and vmp_path.exists():
|
|
||||||
device_kwargs["vmp"] = vmp_path
|
|
||||||
|
|
||||||
device = Device.from_files(**device_kwargs)
|
|
||||||
|
|
||||||
cdm = Cdm.from_device(device)
|
cdm = Cdm.from_device(device)
|
||||||
session_id = cdm.open()
|
session_id = cdm.open()
|
||||||
@@ -119,13 +222,13 @@ session_id = cdm.open()
|
|||||||
try:
|
try:
|
||||||
challenge = cdm.get_license_challenge(session_id, pssh)
|
challenge = cdm.get_license_challenge(session_id, pssh)
|
||||||
|
|
||||||
license_response = requests.post(
|
response = requests.post(
|
||||||
"https://cwip-shaka-proxy.appspot.com/no_auth",
|
"https://cwip-shaka-proxy.appspot.com/no_auth",
|
||||||
data=challenge,
|
data=challenge,
|
||||||
)
|
)
|
||||||
license_response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
cdm.parse_license(session_id, license_response.content)
|
cdm.parse_license(session_id, response.content)
|
||||||
|
|
||||||
for key in cdm.get_keys(session_id):
|
for key in cdm.get_keys(session_id):
|
||||||
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
|
print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
|
||||||
@@ -134,12 +237,95 @@ finally:
|
|||||||
cdm.close(session_id)
|
cdm.close(session_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
This tool is intended strictly for **educational, research, and interoperability purposes**.
|
This tool is intended strictly for **educational, research, and interoperability purposes**.
|
||||||
The author does not support or encourage the misuse of DRM systems or unauthorized access to protected content.
|
The author does not support or encourage misuse of DRM systems or unauthorized access to protected content.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -147,10 +333,8 @@ The author does not support or encourage the misuse of DRM systems or unauthoriz
|
|||||||
|
|
||||||
For bug reports, feature requests, or general inquiries, please open an issue in the repository.
|
For bug reports, feature requests, or general inquiries, please open an issue in the repository.
|
||||||
|
|
||||||
Support and updates will be provided as availability permits.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
This project is **based on pywidevine**, with modifications and restructuring to support standalone usage and extended functionality for working with WVD and PSSH data.
|
This project is based on **pywidevine**, adapted for standalone usage and improved WVD and PSSH workflows.
|
||||||
|
|||||||
Reference in New Issue
Block a user