+ Updated License Protocol

+ Removed 'partial parse' for VmpData and ClientId
This commit is contained in:
larley 2025-08-20 20:23:45 +02:00
parent 97b8a79be8
commit 8b1c068404
8 changed files with 689 additions and 583 deletions

View File

@ -38,8 +38,7 @@ from pywidevine.pssh import PSSH
import requests
# prepare pssh
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
"7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
# load device
device = Device.load("C:/Path/To/A/Provision.wvd")
@ -93,42 +92,6 @@ cdm.close(session_id)
5. All efforts in this project have been the result of Reverse-Engineering, Publicly available research, and Trial
& Error.
## Key and Output Security
*Licenses, Content Keys, and Decrypted Data is not secure in this CDM implementation.*
The Content Decryption Module is meant to do all downloading, decrypting, and decoding of content, not just license
acquisition. This Python implementation only does License Acquisition within the CDM.
The section of which a 'Decrypt Frame' call is made would be more of a 'Decrypt File' in this implementation. Just
returning the original file in plain text defeats the point of the DRM. Even if 'Decrypt File' was somehow secure, the
Content Keys used to decrypt the files are already exposed to the caller anyway, allowing them to manually decrypt.
An attack on a 'Decrypt Frame' system would be analogous to doing an HDMI capture or similar attack. This is because it
would require re-encoding the video by splicing each individual frame with the right frame-rate, syncing to audio, and
more.
While a 'Decrypt Video' system would be analogous to downloading a Video and passing it through a script. Not much of
an attack if at all. The only protection against a system like this would be monitoring the provision and acquisitions
of licenses and prevent them. This can be done by revoking the device provision, or the user or their authorization to
the service.
There isn't any immediate way to secure either Key or Decrypted information within a Python environment that is not
Hardware backed. Even if obfuscation or some other form of Security by Obscurity was used, this is a Software-based
Content Protection Module (in Python no less) with no hardware backed security. It would be incredibly trivial to break
any sort of protection against retrieving the original video data.
Though, it's not impossible. Google's Chrome Browser CDM is a simple library extension file programmed in C++ that has
been improving its security using math and obscurity for years. It's getting harder and harder to break with its latest
versions only being beaten by Brute-force style methods. However, they have a huge team of very skilled workers, and
making a CDM in C++ has immediate security benefits and a lot of methods to obscure and obfuscate the code.
## Contributors
<a href="https://github.com/rlaphoenix"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17136956?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt=""/></a>
<a href="https://github.com/mediaminister"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/45148099?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt=""/></a>
<a href="https://github.com/sr0lle"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/111277375?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt=""/></a>
## Licensing
This software is licensed under the terms of [GNU General Public License, Version 3.0](LICENSE).
@ -140,3 +103,4 @@ You can find a copy of the license in the LICENSE file in the root folder.
* * *
© rlaphoenix 2022-2023
DevLARLEY 2025-2025

View File

@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "pywidevine"
version = "1.8.0"
version = "1.8.1"
description = "Widevine CDM (Content Decryption Module) implementation in Python."
license = "GPL-3.0-only"
authors = ["rlaphoenix <rlaphoenix@pm.me>", "DevLARLEY"]
@ -32,7 +32,7 @@ include = [
[tool.poetry.dependencies]
python = ">=3.8"
protobuf = "^4.25.1"
protobuf = "^6.32.0"
pymp4 = "^1.4.0"
pycryptodome = "^3.19.0"
click = "^8.1.7"

View File

@ -5,4 +5,4 @@ from .pssh import *
from .remotecdm import *
from .session import *
__version__ = "1.8.0"
__version__ = "1.8.1"

View File

@ -14,7 +14,7 @@ from construct import Padded, Padding, Struct, this
from Crypto.PublicKey import RSA
from google.protobuf.message import DecodeError
from pywidevine.license_protocol_pb2 import ClientIdentification, DrmCertificate, FileHashes, SignedDrmCertificate
from pywidevine.license_protocol_pb2 import ClientIdentification, DrmCertificate, VmpData, SignedDrmCertificate
class DeviceTypes(Enum):
@ -107,21 +107,11 @@ class Device:
self.flags = flags or {}
self.private_key = RSA.importKey(private_key)
self.client_id = ClientIdentification()
try:
self.client_id.ParseFromString(client_id)
if self.client_id.SerializeToString() != client_id:
raise DecodeError("partial parse")
except DecodeError as e:
raise DecodeError(f"Failed to parse client_id as a ClientIdentification, {e}")
self.vmp = FileHashes()
self.vmp = VmpData()
if self.client_id.vmp_data:
try:
self.vmp.ParseFromString(self.client_id.vmp_data)
if self.vmp.SerializeToString() != self.client_id.vmp_data:
raise DecodeError("partial parse")
except DecodeError as e:
raise DecodeError(f"Failed to parse Client ID's VMP data as a FileHashes, {e}")
signed_drm_certificate = SignedDrmCertificate()
drm_certificate = DrmCertificate()
@ -202,23 +192,13 @@ class Device:
v1_struct.version = 2 # update version to 2 to allow loading
v1_struct.flags = Container() # blank flags that may have been used in v1
vmp = FileHashes()
vmp = VmpData()
if v1_struct.vmp:
try:
vmp.ParseFromString(v1_struct.vmp)
if vmp.SerializeToString() != v1_struct.vmp:
raise DecodeError("partial parse")
except DecodeError as e:
raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}")
v1_struct.vmp = vmp
client_id = ClientIdentification()
try:
client_id.ParseFromString(v1_struct.client_id)
if client_id.SerializeToString() != v1_struct.client_id:
raise DecodeError("partial parse")
except DecodeError as e:
raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}")
new_vmp_data = v1_struct.vmp.SerializeToString()
if client_id.vmp_data and client_id.vmp_data != new_vmp_data:

View File

@ -5,8 +5,6 @@ package pywidevine_license_protocol;
// need this if we are using libprotobuf-cpp-2.3.0-lite
option optimize_for = LITE_RUNTIME;
option java_package = "com.rlaphoenix.pywidevine.protos";
enum LicenseType {
STREAMING = 1;
OFFLINE = 2;
@ -738,15 +736,45 @@ message WidevinePsshData {
optional bytes grouped_license = 8 [deprecated = true];
}
// File Hashes for Verified Media Path (VMP) support.
message FileHashes {
message Signature {
optional string filename = 1;
optional bool test_signing = 2; //0 - release, 1 - testing
optional bytes SHA512Hash = 3;
optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file
// VmpData for Verified Media Path (VMP) support.
message VmpData {
message SignedBinaryInfo {
enum HashAlgorithmProto {
// Unspecified hash algorithm: SHA_256 shall be used for ECC based algorithms
// and SHA_1 shall be used otherwise.
HASH_ALGORITHM_UNSPECIFIED = 0;
HASH_ALGORITHM_SHA_1 = 1;
HASH_ALGORITHM_SHA_256 = 2;
HASH_ALGORITHM_SHA_384 = 3;
}
// File name of the binary. Required.
optional string file_name = 1;
// Index into |certificates| for the code signing certificate used to sign
// this binary. Required if the binary is signed.
optional uint32 certificate_index = 2;
// SHA-512 digest of signed binary. Required if the file was present.
optional bytes binary_hash = 3;
// Flags from signature file, if any. Required if signed.
optional uint32 flags = 4;
// Signature of the binary. Required if signed.
optional bytes signature = 5;
// Optional field that indicates the hash algorithm used in signature
// scheme.
optional HashAlgorithmProto hash_algorithm = 6;
}
optional bytes signer = 1;
repeated Signature signatures = 2;
// Distinct certificates used in binary code signing. No certificate should
// be present more than once.
repeated bytes certificates = 1;
// Info about each signed binary.
repeated SignedBinaryInfo signed_binary_info = 2;
repeated SignedBinaryInfo cdm_host_files = 3;
repeated SignedBinaryInfo cdm_call_chain_files = 4;
optional SignedBinaryInfo current_process_file = 5;
repeated uint32 host_file_indexes = 6;
repeated uint32 call_chain_file_indexes = 7;
repeated uint32 current_process_file_index = 8;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ from unidecode import UnidecodeError, unidecode
from pywidevine import __version__
from pywidevine.cdm import Cdm
from pywidevine.device import Device, DeviceTypes
from pywidevine.license_protocol_pb2 import FileHashes, LicenseType
from pywidevine.license_protocol_pb2 import VmpData, LicenseType
from pywidevine.pssh import PSSH
@ -169,7 +169,7 @@ def test(ctx: click.Context, device: Path, privacy: bool) -> None:
@click.option("-l", "--level", type=click.IntRange(1, 3), required=True, help="Device Security Level")
@click.option("-k", "--key", type=Path, required=True, help="Device RSA Private Key in PEM or DER format")
@click.option("-c", "--client_id", type=Path, required=True, help="Widevine ClientIdentification Blob file")
@click.option("-v", "--vmp", type=Path, default=None, help="Widevine FileHashes Blob file")
@click.option("-v", "--vmp", type=Path, default=None, help="Widevine VmpData Blob file")
@click.option("-o", "--output", type=Path, default=None, help="Output Path or Directory")
@click.pass_context
def create_device(
@ -247,9 +247,9 @@ def create_device(
log.info(" + Private Key: %s (%s bit)", bool(device.private_key), device.private_key.size_in_bits())
log.info(" + Client ID: %s (%s bytes)", bool(device.client_id), len(device.client_id.SerializeToString()))
if device.client_id.vmp_data:
file_hashes_ = FileHashes()
file_hashes_.ParseFromString(device.client_id.vmp_data)
log.info(" + VMP: True (%s signatures)", len(file_hashes_.signatures))
vmp_data_ = VmpData()
vmp_data_.ParseFromString(device.client_id.vmp_data)
log.info(" + VMP: True (%s signatures)", len(vmp_data_.signed_binary_info))
else:
log.info(" + VMP: False")
log.info(" + Saved to: %s", out_path.absolute())
@ -329,9 +329,9 @@ def export_device(ctx: click.Context, wvd_path: Path, out_dir: Optional[Path] =
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")
log.info("Exported VMP (VmpData) as vmp.bin")
else:
log.info("No VMP (File Hashes) available")
log.info("No VMP (VmpData) available")
@main.command()