Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37d466b9a8 | ||
|
|
05b6753aa6 | ||
|
|
ada7cb009e | ||
|
|
7c91f2c59a | ||
|
|
eaa26399e0 | ||
|
|
74f960aeba | ||
|
|
42b825dcd5 | ||
|
|
fa00bbd8e4 | ||
|
|
a4c6f98650 | ||
|
|
24297d577e | ||
|
|
e90371922c |
30
CHANGELOG.md
30
CHANGELOG.md
@@ -5,6 +5,35 @@ 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.5.1] - 2022-10-23
|
||||
|
||||
### Added
|
||||
|
||||
- Added import path shortcuts in the `__init__.py` package constructor to all the user classes. Now you can do e.g.,
|
||||
`from pywidevine import PSSH` instead of `from pywidevine.pssh import PSSH`. You can still do it both ways.
|
||||
- Improved error handling and sanitization checks when parsing some Service Certificates in `set_service_certificate()`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Maximum concurrent Cdm sessions are now set to 16 as it seems tto be a more common limit on more up-to-date CDMs,
|
||||
including Android's OEMCrypto Library. This also helps encourage people to close their sessions when they are no
|
||||
longer required.
|
||||
- Service Certificates are now stored in the session as a `SignedDrmCertificate`. This is to keep the signature with
|
||||
the stored Certificate for use by the user if necessary. It also reduces code repetition relating to the usage of the
|
||||
signature.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved reliability of computing License Signatures. Some license messages when parsed would be slightly different
|
||||
when re-serialized with `SerializeToString()`, therefore the computed signature would have always mismatched.
|
||||
- Added support for Key IDs that are integer values. Effectively all values are now considered to be a UUID as 16 bytes
|
||||
(in hex or bytes) or an integer value with support for up to 16 bytes. All integer values are converted to a UUID and
|
||||
are loaded big-endian.
|
||||
- Fixed acquisition of the Certificate's provider_id within `set_service_certificate()` in some edge cases, but also
|
||||
when you try to remove the certificate by setting it to `None`.
|
||||
- PSSH now dumps in the same version the PSSH was loaded or created in. Previously it would always dump as a v1 PSSH
|
||||
box due to a cascading check in pymp4. It now also honors the currently set version in the case it gets overridden.
|
||||
|
||||
## [1.5.0] - 2022-09-24
|
||||
|
||||
With just one change this brings along a reduced dependency tree, smoother experience across different platforms, and
|
||||
@@ -296,6 +325,7 @@ This release is primarily a maintenance release for `serve` functionality but so
|
||||
|
||||
Initial Release.
|
||||
|
||||
[1.5.1]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.5.1
|
||||
[1.5.0]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.5.0
|
||||
[1.4.4]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.4.4
|
||||
[1.4.3]: https://github.com/rlaphoenix/pywidevine/releases/tag/v1.4.3
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "pywidevine"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
description = "Widevine CDM (Content Decryption Module) implementation in Python."
|
||||
authors = ["rlaphoenix <rlaphoenix@pm.me>"]
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
__version__ = "1.5.0"
|
||||
from .cdm import *
|
||||
from .device import *
|
||||
from .key import *
|
||||
from .pssh import *
|
||||
from .remotecdm import *
|
||||
from .session import *
|
||||
|
||||
|
||||
__version__ = "1.5.1"
|
||||
|
||||
@@ -62,7 +62,7 @@ class Cdm:
|
||||
root_cert = DrmCertificate()
|
||||
root_cert.ParseFromString(root_signed_cert.drm_certificate)
|
||||
|
||||
MAX_NUM_OF_SESSIONS = 50 # most common limit
|
||||
MAX_NUM_OF_SESSIONS = 16
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -185,10 +185,14 @@ class Cdm:
|
||||
raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
|
||||
|
||||
if certificate is None:
|
||||
drm_certificate = DrmCertificate()
|
||||
drm_certificate.ParseFromString(session.service_certificate.drm_certificate)
|
||||
if session.service_certificate:
|
||||
drm_certificate = DrmCertificate()
|
||||
drm_certificate.ParseFromString(session.service_certificate.drm_certificate)
|
||||
provider_id = drm_certificate.provider_id
|
||||
else:
|
||||
provider_id = None
|
||||
session.service_certificate = None
|
||||
return drm_certificate.provider_id
|
||||
return provider_id
|
||||
|
||||
if isinstance(certificate, str):
|
||||
try:
|
||||
@@ -200,6 +204,7 @@ class Cdm:
|
||||
|
||||
signed_message = SignedMessage()
|
||||
signed_drm_certificate = SignedDrmCertificate()
|
||||
drm_certificate = DrmCertificate()
|
||||
|
||||
try:
|
||||
signed_message.ParseFromString(certificate)
|
||||
@@ -209,10 +214,6 @@ class Cdm:
|
||||
signed_drm_certificate.ParseFromString(certificate)
|
||||
if signed_drm_certificate.SerializeToString() != certificate:
|
||||
raise DecodeError("partial parse")
|
||||
# Craft a SignedMessage as it's stored as a SignedMessage
|
||||
signed_message.Clear()
|
||||
signed_message.msg = signed_drm_certificate.SerializeToString()
|
||||
# we don't need to sign this message, this is normal
|
||||
except DecodeError as e:
|
||||
# could be a direct unsigned DrmCertificate, but reject those anyway
|
||||
raise DecodeError(f"Could not parse certificate as a SignedDrmCertificate, {e}")
|
||||
@@ -226,13 +227,20 @@ class Cdm:
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
raise SignatureMismatch("Signature Mismatch on SignedDrmCertificate, rejecting certificate")
|
||||
else:
|
||||
session.service_certificate = signed_message
|
||||
drm_certificate = DrmCertificate()
|
||||
drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate)
|
||||
return drm_certificate.provider_id
|
||||
|
||||
def get_service_certificate(self, session_id: bytes) -> Optional[SignedMessage]:
|
||||
try:
|
||||
drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate)
|
||||
if drm_certificate.SerializeToString() != signed_drm_certificate.drm_certificate:
|
||||
raise DecodeError("partial parse")
|
||||
except DecodeError as e:
|
||||
raise DecodeError(f"Could not parse signed certificate's message as a DrmCertificate, {e}")
|
||||
|
||||
# must be stored as a SignedDrmCertificate as the signature needs to be kept for RemoteCdm
|
||||
# if we store as DrmCertificate (no signature) then RemoteCdm cannot verify the Certificate
|
||||
session.service_certificate = signed_drm_certificate
|
||||
return drm_certificate.provider_id
|
||||
|
||||
def get_service_certificate(self, session_id: bytes) -> Optional[SignedDrmCertificate]:
|
||||
"""
|
||||
Get the currently set Service Privacy Certificate of the Session.
|
||||
|
||||
@@ -405,9 +413,12 @@ class Cdm:
|
||||
key=self.__decrypter.decrypt(license_message.session_key)
|
||||
)
|
||||
|
||||
# explicitly use the original `license_message.msg` instead of a re-serializing from `licence`
|
||||
# as some differences may end up in the output due to differences in the proto
|
||||
|
||||
computed_signature = HMAC. \
|
||||
new(mac_key_server, digestmod=SHA256). \
|
||||
update(licence.SerializeToString()). \
|
||||
update(license_message.msg). \
|
||||
digest()
|
||||
|
||||
if license_message.signature != computed_signature:
|
||||
@@ -541,7 +552,7 @@ class Cdm:
|
||||
@staticmethod
|
||||
def encrypt_client_id(
|
||||
client_id: ClientIdentification,
|
||||
service_certificate: Union[SignedMessage, SignedDrmCertificate, DrmCertificate],
|
||||
service_certificate: Union[SignedDrmCertificate, DrmCertificate],
|
||||
key: bytes = None,
|
||||
iv: bytes = None
|
||||
) -> EncryptedClientIdentification:
|
||||
@@ -549,10 +560,6 @@ class Cdm:
|
||||
privacy_key = key or get_random_bytes(16)
|
||||
privacy_iv = iv or get_random_bytes(16)
|
||||
|
||||
if isinstance(service_certificate, SignedMessage):
|
||||
signed_drm_certificate = SignedDrmCertificate()
|
||||
signed_drm_certificate.ParseFromString(service_certificate.msg)
|
||||
service_certificate = signed_drm_certificate
|
||||
if isinstance(service_certificate, SignedDrmCertificate):
|
||||
drm_certificate = DrmCertificate()
|
||||
drm_certificate.ParseFromString(service_certificate.drm_certificate)
|
||||
|
||||
@@ -19,8 +19,8 @@ class PSSH:
|
||||
"""PSSH-related utilities. Somewhat Widevine-biased."""
|
||||
|
||||
class SystemId:
|
||||
Widevine = UUID(bytes=b"\xed\xef\x8b\xa9\x79\xd6\x4a\xce\xa3\xc8\x27\xdc\xd5\x1d\x21\xed")
|
||||
PlayReady = UUID(bytes=b"\x9a\x04\xf0\x79\x98\x40\x42\x86\xab\x92\xe6\x5b\xe0\x88\x5f\x95")
|
||||
Widevine = UUID(hex="edef8ba979d64acea3c827dcd51d21ed")
|
||||
PlayReady = UUID(hex="9a04f07998404286ab92e65be0885f95")
|
||||
|
||||
def __init__(self, data: Union[Container, str, bytes], strict: bool = False):
|
||||
"""
|
||||
@@ -199,7 +199,11 @@ class PSSH:
|
||||
cenc_header.ParseFromString(self.init_data)
|
||||
return [
|
||||
# the key_ids value may or may not be hex underlying
|
||||
UUID(bytes=key_id) if len(key_id) == 16 else UUID(hex=key_id.decode())
|
||||
(
|
||||
UUID(bytes=key_id) if len(key_id) == 16 else # normal
|
||||
UUID(hex=key_id.decode()) if len(key_id) == 32 else # stored as hex
|
||||
UUID(int=int.from_bytes(key_id, "big")) # assuming as number
|
||||
)
|
||||
for key_id in cenc_header.key_ids
|
||||
]
|
||||
|
||||
@@ -231,7 +235,7 @@ class PSSH:
|
||||
version=self.version,
|
||||
flags=self.flags,
|
||||
system_ID=self.system_id,
|
||||
key_IDs=self.key_ids,
|
||||
key_IDs=self.key_ids if self.version == 1 and self.key_ids else None,
|
||||
init_data=self.init_data
|
||||
))
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ class RemoteCdm(Cdm):
|
||||
|
||||
return r["provider_id"]
|
||||
|
||||
def get_service_certificate(self, session_id: bytes) -> Optional[SignedMessage]:
|
||||
def get_service_certificate(self, session_id: bytes) -> Optional[SignedDrmCertificate]:
|
||||
r = self.__session.post(
|
||||
url=f"{self.host}/{self.device_name}/get_service_certificate",
|
||||
json={
|
||||
@@ -159,21 +159,12 @@ class RemoteCdm(Cdm):
|
||||
return None
|
||||
|
||||
service_certificate = base64.b64decode(service_certificate)
|
||||
signed_message = SignedMessage()
|
||||
signed_drm_certificate = SignedDrmCertificate()
|
||||
|
||||
try:
|
||||
signed_message.ParseFromString(service_certificate)
|
||||
if signed_message.SerializeToString() == service_certificate:
|
||||
signed_drm_certificate.ParseFromString(signed_message.msg)
|
||||
else:
|
||||
signed_drm_certificate.ParseFromString(service_certificate)
|
||||
if signed_drm_certificate.SerializeToString() != service_certificate:
|
||||
raise DecodeError("partial parse")
|
||||
# Craft a SignedMessage as it's stored as a SignedMessage
|
||||
signed_message.Clear()
|
||||
signed_message.msg = signed_drm_certificate.SerializeToString()
|
||||
# we don't need to sign this message, this is normal
|
||||
signed_drm_certificate.ParseFromString(service_certificate)
|
||||
if signed_drm_certificate.SerializeToString() != service_certificate:
|
||||
raise DecodeError("partial parse")
|
||||
except DecodeError as e:
|
||||
# could be a direct unsigned DrmCertificate, but reject those anyway
|
||||
raise DecodeError(f"Could not parse certificate as a SignedDrmCertificate, {e}")
|
||||
@@ -187,8 +178,8 @@ class RemoteCdm(Cdm):
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
raise SignatureMismatch("Signature Mismatch on SignedDrmCertificate, rejecting certificate")
|
||||
else:
|
||||
return signed_message
|
||||
|
||||
return signed_drm_certificate
|
||||
|
||||
def get_license_challenge(
|
||||
self,
|
||||
|
||||
@@ -3,13 +3,13 @@ from typing import Optional
|
||||
from Crypto.Random import get_random_bytes
|
||||
|
||||
from pywidevine.key import Key
|
||||
from pywidevine.license_protocol_pb2 import SignedMessage
|
||||
from pywidevine.license_protocol_pb2 import SignedDrmCertificate
|
||||
|
||||
|
||||
class Session:
|
||||
def __init__(self, number: int):
|
||||
self.number = number
|
||||
self.id = get_random_bytes(16)
|
||||
self.service_certificate: Optional[SignedMessage] = None
|
||||
self.service_certificate: Optional[SignedDrmCertificate] = None
|
||||
self.context: dict[bytes, tuple[bytes, bytes]] = {}
|
||||
self.keys: list[Key] = []
|
||||
|
||||
Reference in New Issue
Block a user