+ 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
|
import requests
|
||||||
|
|
||||||
# prepare pssh
|
# prepare pssh
|
||||||
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
|
pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
||||||
"7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
|
|
||||||
|
|
||||||
# load device
|
# load device
|
||||||
device = Device.load("C:/Path/To/A/Provision.wvd")
|
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
|
5. All efforts in this project have been the result of Reverse-Engineering, Publicly available research, and Trial
|
||||||
& Error.
|
& 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
|
## Licensing
|
||||||
|
|
||||||
This software is licensed under the terms of [GNU General Public License, Version 3.0](LICENSE).
|
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
|
© rlaphoenix 2022-2023
|
||||||
|
DevLARLEY 2025-2025
|
||||||
|
|||||||
@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pywidevine"
|
name = "pywidevine"
|
||||||
version = "1.8.0"
|
version = "1.8.1"
|
||||||
description = "Widevine CDM (Content Decryption Module) implementation in Python."
|
description = "Widevine CDM (Content Decryption Module) implementation in Python."
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
authors = ["rlaphoenix <rlaphoenix@pm.me>", "DevLARLEY"]
|
authors = ["rlaphoenix <rlaphoenix@pm.me>", "DevLARLEY"]
|
||||||
@ -32,7 +32,7 @@ include = [
|
|||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.8"
|
python = ">=3.8"
|
||||||
protobuf = "^4.25.1"
|
protobuf = "^6.32.0"
|
||||||
pymp4 = "^1.4.0"
|
pymp4 = "^1.4.0"
|
||||||
pycryptodome = "^3.19.0"
|
pycryptodome = "^3.19.0"
|
||||||
click = "^8.1.7"
|
click = "^8.1.7"
|
||||||
|
|||||||
@ -5,4 +5,4 @@ from .pssh import *
|
|||||||
from .remotecdm import *
|
from .remotecdm import *
|
||||||
from .session 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 Crypto.PublicKey import RSA
|
||||||
from google.protobuf.message import DecodeError
|
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):
|
class DeviceTypes(Enum):
|
||||||
@ -107,21 +107,11 @@ class Device:
|
|||||||
self.flags = flags or {}
|
self.flags = flags or {}
|
||||||
self.private_key = RSA.importKey(private_key)
|
self.private_key = RSA.importKey(private_key)
|
||||||
self.client_id = ClientIdentification()
|
self.client_id = ClientIdentification()
|
||||||
try:
|
self.client_id.ParseFromString(client_id)
|
||||||
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:
|
if self.client_id.vmp_data:
|
||||||
try:
|
self.vmp.ParseFromString(self.client_id.vmp_data)
|
||||||
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()
|
signed_drm_certificate = SignedDrmCertificate()
|
||||||
drm_certificate = DrmCertificate()
|
drm_certificate = DrmCertificate()
|
||||||
@ -202,23 +192,13 @@ class Device:
|
|||||||
v1_struct.version = 2 # update version to 2 to allow loading
|
v1_struct.version = 2 # update version to 2 to allow loading
|
||||||
v1_struct.flags = Container() # blank flags that may have been used in v1
|
v1_struct.flags = Container() # blank flags that may have been used in v1
|
||||||
|
|
||||||
vmp = FileHashes()
|
vmp = VmpData()
|
||||||
if v1_struct.vmp:
|
if v1_struct.vmp:
|
||||||
try:
|
vmp.ParseFromString(v1_struct.vmp)
|
||||||
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
|
v1_struct.vmp = vmp
|
||||||
|
|
||||||
client_id = ClientIdentification()
|
client_id = ClientIdentification()
|
||||||
try:
|
client_id.ParseFromString(v1_struct.client_id)
|
||||||
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()
|
new_vmp_data = v1_struct.vmp.SerializeToString()
|
||||||
if client_id.vmp_data and client_id.vmp_data != new_vmp_data:
|
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
|
// need this if we are using libprotobuf-cpp-2.3.0-lite
|
||||||
option optimize_for = LITE_RUNTIME;
|
option optimize_for = LITE_RUNTIME;
|
||||||
|
|
||||||
option java_package = "com.rlaphoenix.pywidevine.protos";
|
|
||||||
|
|
||||||
enum LicenseType {
|
enum LicenseType {
|
||||||
STREAMING = 1;
|
STREAMING = 1;
|
||||||
OFFLINE = 2;
|
OFFLINE = 2;
|
||||||
@ -738,15 +736,45 @@ message WidevinePsshData {
|
|||||||
optional bytes grouped_license = 8 [deprecated = true];
|
optional bytes grouped_license = 8 [deprecated = true];
|
||||||
}
|
}
|
||||||
|
|
||||||
// File Hashes for Verified Media Path (VMP) support.
|
// VmpData for Verified Media Path (VMP) support.
|
||||||
message FileHashes {
|
message VmpData {
|
||||||
message Signature {
|
message SignedBinaryInfo {
|
||||||
optional string filename = 1;
|
enum HashAlgorithmProto {
|
||||||
optional bool test_signing = 2; //0 - release, 1 - testing
|
// Unspecified hash algorithm: SHA_256 shall be used for ECC based algorithms
|
||||||
optional bytes SHA512Hash = 3;
|
// and SHA_1 shall be used otherwise.
|
||||||
optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file
|
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 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 import __version__
|
||||||
from pywidevine.cdm import Cdm
|
from pywidevine.cdm import Cdm
|
||||||
from pywidevine.device import Device, DeviceTypes
|
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
|
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("-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("-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("-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.option("-o", "--output", type=Path, default=None, help="Output Path or Directory")
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def create_device(
|
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(" + 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()))
|
log.info(" + Client ID: %s (%s bytes)", bool(device.client_id), len(device.client_id.SerializeToString()))
|
||||||
if device.client_id.vmp_data:
|
if device.client_id.vmp_data:
|
||||||
file_hashes_ = FileHashes()
|
vmp_data_ = VmpData()
|
||||||
file_hashes_.ParseFromString(device.client_id.vmp_data)
|
vmp_data_.ParseFromString(device.client_id.vmp_data)
|
||||||
log.info(" + VMP: True (%s signatures)", len(file_hashes_.signatures))
|
log.info(" + VMP: True (%s signatures)", len(vmp_data_.signed_binary_info))
|
||||||
else:
|
else:
|
||||||
log.info(" + VMP: False")
|
log.info(" + VMP: False")
|
||||||
log.info(" + Saved to: %s", out_path.absolute())
|
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:
|
if device.client_id.vmp_data:
|
||||||
vmp_path = out_path / "vmp.bin"
|
vmp_path = out_path / "vmp.bin"
|
||||||
vmp_path.write_bytes(device.client_id.vmp_data)
|
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:
|
else:
|
||||||
log.info("No VMP (File Hashes) available")
|
log.info("No VMP (VmpData) available")
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user