Add export-device command to export WVDs back as files

In reality you wouldn't need this for use with pywidevine, but a lot have asked me for this feature so they can use WVDs in other ways or with other software that does not support WVDs.
This commit is contained in:
rlaphoenix 2023-02-03 06:53:55 +00:00
parent fd3df13e9c
commit 99aef63354
3 changed files with 84 additions and 3 deletions

4
poetry.lock generated
View File

@ -572,7 +572,7 @@ name = "pyyaml"
version = "6.0" version = "6.0"
description = "YAML parser and emitter for Python" description = "YAML parser and emitter for Python"
category = "main" category = "main"
optional = true optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
@ -789,4 +789,4 @@ serve = ["aiohttp", "PyYAML"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.7,<3.12" python-versions = ">=3.7,<3.12"
content-hash = "4ac0b422601d094bfbda39c94bea40171e995b7e9bd4b97b4d55dd34f5af7360" content-hash = "c392d2830d8a0614ebdaa8b16fce5ccd0f92020db948270cb57246fe4c7b1372"

View File

@ -35,8 +35,8 @@ click = "^8.1.3"
requests = "^2.28.1" requests = "^2.28.1"
lxml = ">=4.9.2" lxml = ">=4.9.2"
Unidecode = "^1.3.4" Unidecode = "^1.3.4"
PyYAML = "^6.0"
aiohttp = {version = "^3.8.1", optional = true} aiohttp = {version = "^3.8.1", optional = true}
PyYAML = {version = "^6.0", optional = true}
[tool.poetry.extras] [tool.poetry.extras]
serve = ["aiohttp", "PyYAML"] serve = ["aiohttp", "PyYAML"]

View File

@ -8,6 +8,8 @@ import click
import requests import requests
from construct import ConstructError from construct import ConstructError
from unidecode import unidecode, UnidecodeError from unidecode import unidecode, UnidecodeError
import yaml
from google.protobuf.json_format import MessageToDict
from pywidevine import __version__ from pywidevine import __version__
from pywidevine.cdm import Cdm from pywidevine.cdm import Cdm
@ -247,6 +249,85 @@ def create_device(
log.info(" + Saved to: %s", out_path.absolute()) log.info(" + Saved to: %s", out_path.absolute())
@main.command()
@click.argument("wvd_path", type=Path)
@click.option("-o", "--out_dir", type=Path, default=None, help="Output Directory")
@click.pass_context
def export_device(ctx: click.Context, wvd_path: Path, out_dir: Optional[Path] = None) -> None:
"""
Export a Widevine Device (.wvd) file to an RSA Private Key (PEM and DER) and Client ID Blob.
Optionally also a VMP (Verified Media Path) Blob, which will be stored in the Client ID.
If an output directory is not specified, it will be stored in the current working directory.
"""
if not wvd_path.is_file():
raise click.UsageError("wvd_path: Not a path to a file, or it doesn't exist.", ctx)
log = logging.getLogger("export-device")
log.info("Exporting Widevine Device (.wvd) file, %s", wvd_path.stem)
if not out_dir:
out_dir = Path.cwd()
out_path = out_dir / wvd_path.stem
if out_path.exists():
if any(out_path.iterdir()):
log.error("Output directory is not empty, cannot overwrite.")
return
else:
log.warning("Output directory already exists, but is empty.")
else:
out_path.mkdir(parents=True)
device = Device.load(wvd_path)
log.info(f"L{device.security_level} {device.system_id} {device.type.name}")
log.info(f"Saving to: {out_path}")
device_meta = {
"wvd": {
"device_type": device.type.name,
"security_level": device.security_level,
**device.flags
},
"client_info": {},
"capabilities": MessageToDict(device.client_id, preserving_proto_field_name=True)["client_capabilities"]
}
for client_info in device.client_id.client_info:
device_meta["client_info"][client_info.name] = client_info.value
device_meta_path = out_path / "metadata.yml"
device_meta_path.write_text(yaml.dump(device_meta), encoding="utf8")
log.info("Exported Device Metadata as metadata.yml")
if device.private_key:
private_key_path = out_path / "private_key.pem"
private_key_path.write_text(
data=device.private_key.export_key().decode(),
encoding="utf8"
)
private_key_path.with_suffix(".der").write_bytes(
device.private_key.export_key(format="DER")
)
log.info("Exported Private Key as private_key.der and private_key.pem")
else:
log.warning("No Private Key available")
if device.client_id:
client_id_path = out_path / "client_id.bin"
client_id_path.write_bytes(device.client_id.SerializeToString())
log.info("Exported Client ID as client_id.bin")
else:
log.warning("No Client ID available")
if device.client_id.vmp_data:
vmp_path = out_path / "vmp.bin"
vmp_path.write_bytes(device.client_id.vmp_data)
log.info("Exported VMP (File Hashes) as vmp.bin")
else:
log.info("No VMP (File Hashes) available")
@main.command() @main.command()
@click.argument("path", type=Path) @click.argument("path", type=Path)
@click.pass_context @click.pass_context