+ PSSH class overhaul
This commit is contained in:
parent
887a7cd38f
commit
d6cbc7f867
|
@ -10,4 +10,4 @@ from .session import *
|
||||||
from .xml_key import *
|
from .xml_key import *
|
||||||
from .xmrlicense import *
|
from .xmrlicense import *
|
||||||
|
|
||||||
__version__ = "0.3.7"
|
__version__ = "0.3.8"
|
||||||
|
|
|
@ -10,6 +10,10 @@ class InvalidSession(PyPlayreadyException):
|
||||||
"""No Session is open with the specified identifier."""
|
"""No Session is open with the specified identifier."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPssh(PyPlayreadyException):
|
||||||
|
"""The Playready PSSH is invalid or empty."""
|
||||||
|
|
||||||
|
|
||||||
class InvalidInitData(PyPlayreadyException):
|
class InvalidInitData(PyPlayreadyException):
|
||||||
"""The Playready Cenc Header Data is invalid or empty."""
|
"""The Playready Cenc Header Data is invalid or empty."""
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import base64
|
import base64
|
||||||
from typing import Union
|
from typing import Union, List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from construct import Struct, Int32ul, Int16ul, Array, this, Bytes, Switch, Int32ub, Const, Container
|
from construct import Struct, Int32ul, Int16ul, Array, this, Bytes, Switch, Int32ub, Const, Container, ConstructError
|
||||||
|
|
||||||
|
from pyplayready.exceptions import InvalidPssh
|
||||||
from pyplayready.wrmheader import WRMHeader
|
from pyplayready.wrmheader import WRMHeader
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ class _PlayreadyPSSHStructs:
|
||||||
"data" / Switch(
|
"data" / Switch(
|
||||||
this.type,
|
this.type,
|
||||||
{
|
{
|
||||||
1: Bytes(this.length * 2)
|
1: Bytes(this.length)
|
||||||
},
|
},
|
||||||
default=Bytes(this.length)
|
default=Bytes(this.length)
|
||||||
)
|
)
|
||||||
|
@ -36,64 +37,54 @@ class _PlayreadyPSSHStructs:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PSSH:
|
class PSSH(_PlayreadyPSSHStructs):
|
||||||
SYSTEM_ID = UUID(hex="9a04f07998404286ab92e65be0885f95")
|
SYSTEM_ID = UUID(hex="9a04f07998404286ab92e65be0885f95")
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, data: Union[str, bytes]):
|
||||||
self,
|
|
||||||
data: Union[str, bytes]
|
|
||||||
):
|
|
||||||
"""Represents a PlayReady PSSH"""
|
"""Represents a PlayReady PSSH"""
|
||||||
if not data:
|
if not data:
|
||||||
raise ValueError("Data must not be empty")
|
raise InvalidPssh("Data must not be empty")
|
||||||
|
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
try:
|
try:
|
||||||
data = base64.b64decode(data)
|
data = base64.b64decode(data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Could not decode data as Base64, {e}")
|
raise InvalidPssh(f"Could not decode data as Base64, {e}")
|
||||||
|
|
||||||
|
self.wrm_headers: List[WRMHeader]
|
||||||
try:
|
try:
|
||||||
if self._is_playready_pssh_box(data):
|
# PSSH Box -> PlayReady Header
|
||||||
pssh_box = _PlayreadyPSSHStructs.PSSHBox.parse(data)
|
box = self.PSSHBox.parse(data)
|
||||||
if bool(self._is_utf_16(pssh_box.data)):
|
prh = self.PlayreadyHeader.parse(box.data)
|
||||||
self._wrm_headers = [pssh_box.data.decode("utf-16-le")]
|
self.wrm_headers = self._read_playready_objects(prh)
|
||||||
elif bool(self._is_utf_16(pssh_box.data[6:])):
|
except ConstructError:
|
||||||
self._wrm_headers = [pssh_box.data[6:].decode("utf-16-le")]
|
if int.from_bytes(data[:2], byteorder="little") > 3:
|
||||||
elif bool(self._is_utf_16(pssh_box.data[10:])):
|
try:
|
||||||
self._wrm_headers = [pssh_box.data[10:].decode("utf-16-le")]
|
# PlayReady Header
|
||||||
else:
|
prh = self.PlayreadyHeader.parse(data)
|
||||||
self._wrm_headers = list(self._read_wrm_headers(_PlayreadyPSSHStructs.PlayreadyHeader.parse(pssh_box.data)))
|
self.wrm_headers = self._read_playready_objects(prh)
|
||||||
elif bool(self._is_utf_16(data)):
|
except ConstructError:
|
||||||
self._wrm_headers = [data.decode("utf-16-le")]
|
raise InvalidPssh("Could not parse data as a PSSH Box nor a PlayReady Header")
|
||||||
elif bool(self._is_utf_16(data[6:])):
|
|
||||||
self._wrm_headers = [data[6:].decode("utf-16-le")]
|
|
||||||
elif bool(self._is_utf_16(data[10:])):
|
|
||||||
self._wrm_headers = [data[10:].decode("utf-16-le")]
|
|
||||||
else:
|
else:
|
||||||
self._wrm_headers = list(self._read_wrm_headers(_PlayreadyPSSHStructs.PlayreadyHeader.parse(data)))
|
try:
|
||||||
except Exception:
|
# PlayReady Object
|
||||||
raise Exception("Could not parse data as a PSSH Box nor a PlayReadyHeader")
|
pro = self.PlayreadyObject.parse(data)
|
||||||
|
self.wrm_headers = [WRMHeader(pro.data)]
|
||||||
|
except ConstructError:
|
||||||
|
raise InvalidPssh("Could not parse data as a PSSH Box nor a PlayReady Object")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _downgrade(wrm_header: str) -> str:
|
def _read_playready_objects(header: Container) -> List[WRMHeader]:
|
||||||
return WRMHeader(wrm_header).to_v4_0_0_0()
|
return list(map(
|
||||||
|
lambda pro: WRMHeader(pro.data),
|
||||||
|
filter(
|
||||||
|
lambda pro: pro.type == 1,
|
||||||
|
header.records
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
def get_wrm_headers(self, downgrade_to_v4: bool = False):
|
def get_wrm_headers(self, downgrade_to_v4: bool = False):
|
||||||
return list(map(
|
return list(map(
|
||||||
self._downgrade if downgrade_to_v4 else (lambda _: _),
|
lambda wrm_header: wrm_header.to_v4_0_0_0() if downgrade_to_v4 else wrm_header.dumps(),
|
||||||
self._wrm_headers
|
self.wrm_headers
|
||||||
))
|
))
|
||||||
|
|
||||||
def _is_playready_pssh_box(self, data: bytes) -> bool:
|
|
||||||
return data[12:28] == self.SYSTEM_ID.bytes
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_utf_16(data: bytes) -> bool:
|
|
||||||
return all(map(lambda i: data[i] == 0, range(1, len(data), 2)))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _read_wrm_headers(wrm_header: Container):
|
|
||||||
for record in wrm_header.records:
|
|
||||||
if record.type == 1:
|
|
||||||
yield record.data.decode("utf-16-le")
|
|
||||||
|
|
|
@ -17,6 +17,9 @@ class WRMHeader:
|
||||||
self.value = value
|
self.value = value
|
||||||
self.checksum = checksum
|
self.checksum = checksum
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f'SignedKeyID(alg_id={self.alg_id}, value="{self.value}", checksum="{self.checksum}")'
|
||||||
|
|
||||||
class Version(Enum):
|
class Version(Enum):
|
||||||
VERSION_4_0_0_0 = "4.0.0.0"
|
VERSION_4_0_0_0 = "4.0.0.0"
|
||||||
VERSION_4_1_0_0 = "4.1.0.0"
|
VERSION_4_1_0_0 = "4.1.0.0"
|
||||||
|
@ -184,4 +187,4 @@ class WRMHeader:
|
||||||
)
|
)
|
||||||
|
|
||||||
def dumps(self) -> str:
|
def dumps(self) -> str:
|
||||||
return self._raw_data.decode()
|
return self._raw_data.decode("utf-16-le")
|
||||||
|
|
|
@ -4,8 +4,7 @@ import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from construct import Const, GreedyRange, Struct, Int32ub, Bytes, Int16ub, this, Switch, LazyBound, Array, Container, \
|
from construct import Const, GreedyRange, Struct, Int32ub, Bytes, Int16ub, this, Switch, LazyBound, Array, Container
|
||||||
If, Byte
|
|
||||||
|
|
||||||
|
|
||||||
class _XMRLicenseStructs:
|
class _XMRLicenseStructs:
|
||||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pyplayready"
|
name = "pyplayready"
|
||||||
version = "0.3.7"
|
version = "0.3.8"
|
||||||
description = "pyplayready CDM (Content Decryption Module) implementation in Python."
|
description = "pyplayready CDM (Content Decryption Module) implementation in Python."
|
||||||
license = "CC BY-NC-ND 4.0"
|
license = "CC BY-NC-ND 4.0"
|
||||||
authors = ["DevLARLEY, Erevoc", "DevataDev"]
|
authors = ["DevLARLEY, Erevoc", "DevataDev"]
|
||||||
|
|
Loading…
Reference in New Issue