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:
parent
fd3df13e9c
commit
99aef63354
|
@ -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"
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue