+ Updated License Protocol
+ Removed 'partial parse' for VmpData and ClientId
This commit is contained in:
parent
97b8a79be8
commit
8b1c068404
40
README.md
40
README.md
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -5,4 +5,4 @@ from .pssh import *
|
||||
from .remotecdm import *
|
||||
from .session import *
|
||||
|
||||
__version__ = "1.8.0"
|
||||
__version__ = "1.8.1"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user