Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4a8316227 | ||
|
|
9568d7fdb9 | ||
|
|
ece0914920 | ||
|
|
2ab659eab6 | ||
|
|
99aef63354 | ||
|
|
fd3df13e9c |
4
.github/workflows/cd.yml
vendored
4
.github/workflows/cd.yml
vendored
@@ -14,11 +14,11 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.10.x'
|
||||
python-version: '3.11.x'
|
||||
- name: Install Poetry
|
||||
uses: abatilo/actions-poetry@v2.1.0
|
||||
with:
|
||||
poetry-version: '1.1.11'
|
||||
poetry-version: '1.3.2'
|
||||
- name: Configure poetry
|
||||
run: poetry config virtualenvs.in-project true
|
||||
- name: Install dependencies
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -13,8 +13,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10']
|
||||
poetry-version: [1.1.11]
|
||||
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
|
||||
poetry-version: [1.3.2]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.6.0] - 2023-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- Added full support for Python 3.11.
|
||||
- Added new `export-device` command-line function to export WVD files back as files. I.e., a private key and client ID
|
||||
blob file.
|
||||
|
||||
### Changed
|
||||
|
||||
- The PyYAML dependency is now required even if you do not install Pywidevine with the `serve` extra dependencies.
|
||||
- This was required for exporting WVD metadata in the new `export-device` command-line function.
|
||||
|
||||
## [1.5.3] - 2022-12-27
|
||||
|
||||
### Added
|
||||
@@ -359,6 +372,7 @@ This release is primarily a maintenance release for `serve` functionality but so
|
||||
|
||||
Initial Release.
|
||||
|
||||
[1.6.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.6.0
|
||||
[1.5.3]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.5.3
|
||||
[1.5.2]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.5.2
|
||||
[1.5.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.5.1
|
||||
|
||||
1577
poetry.lock
generated
1577
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "pywidevine"
|
||||
version = "1.5.3"
|
||||
version = "1.6.0"
|
||||
description = "Widevine CDM (Content Decryption Module) implementation in Python."
|
||||
authors = ["rlaphoenix <rlaphoenix@pm.me>"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -27,16 +27,16 @@ classifiers = [
|
||||
"Changelog" = "https://github.com/rlaphoenix/pywidevine/blob/master/CHANGELOG.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.7,<3.11"
|
||||
python = ">=3.7,<3.12"
|
||||
protobuf = "4.21.6"
|
||||
pymp4 = "^1.2.0"
|
||||
pycryptodome = "^3.15.0"
|
||||
click = "^8.1.3"
|
||||
requests = "^2.28.1"
|
||||
lxml = ">=4.9.1"
|
||||
lxml = ">=4.9.2"
|
||||
Unidecode = "^1.3.4"
|
||||
PyYAML = "^6.0"
|
||||
aiohttp = {version = "^3.8.1", optional = true}
|
||||
PyYAML = {version = "^6.0", optional = true}
|
||||
|
||||
[tool.poetry.extras]
|
||||
serve = ["aiohttp", "PyYAML"]
|
||||
|
||||
@@ -8,6 +8,8 @@ import click
|
||||
import requests
|
||||
from construct import ConstructError
|
||||
from unidecode import unidecode, UnidecodeError
|
||||
import yaml
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
|
||||
from pywidevine import __version__
|
||||
from pywidevine.cdm import Cdm
|
||||
@@ -247,6 +249,85 @@ def create_device(
|
||||
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()
|
||||
@click.argument("path", type=Path)
|
||||
@click.pass_context
|
||||
|
||||
Reference in New Issue
Block a user