pyplayready/pyplayready/system/pssh.py
larley fa01639834 + Improved certificate chain validation
+ Added CLI function for creating v2 devices
+ Added CLI function for inspecting certificate chains
2025-08-20 21:32:47 +02:00

109 lines
3.4 KiB
Python

import base64
from typing import Union, List
from uuid import UUID
from construct import Struct, Int32ul, Int16ul, this, Bytes, Switch, Int8ub, Int24ub, Int32ub, Const, Container, \
ConstructError, Rebuild, Default, If, PrefixedArray, Prefixed, GreedyBytes
from pyplayready.misc.exceptions import InvalidPssh
from pyplayready.system.wrmheader import WRMHeader
class _PlayreadyPSSHStructs:
PsshBox = Struct(
"length" / Int32ub,
"pssh" / Const(b"pssh"),
"version" / Rebuild(Int8ub, lambda ctx: 1 if (hasattr(ctx, "key_ids") and ctx.key_ids) else 0),
"flags" / Const(Int24ub, 0),
"system_id" / Bytes(16),
"key_ids" / Default(If(this.version == 1, PrefixedArray(Int32ub, Bytes(16))), None),
"data" / Prefixed(Int32ub, GreedyBytes)
)
PlayreadyObject = Struct(
"type" / Int16ul,
"length" / Int16ul,
"data" / Switch(
this.type,
{
1: Bytes(this.length)
},
default=Bytes(this.length)
)
)
PlayreadyHeader = Struct(
"length" / Int32ul,
"records" / PrefixedArray(Int16ul, PlayreadyObject)
)
class PSSH(_PlayreadyPSSHStructs):
"""Represents a PlayReady PSSH"""
SYSTEM_ID = UUID(hex="9a04f07998404286ab92e65be0885f95")
def __init__(self, data: Union[str, bytes]):
"""Load a PSSH Box, PlayReady Header or PlayReady Object"""
if not data:
raise InvalidPssh("Data must not be empty")
if isinstance(data, str):
try:
data = base64.b64decode(data)
except Exception as e:
raise InvalidPssh(f"Could not decode data as Base64, {e}")
self.wrm_headers: List[WRMHeader]
try:
# PSSH Box -> PlayReady Header
box = self.PsshBox.parse(data)
if self._is_utf_16_le(box.data):
self.wrm_headers = [WRMHeader(box.data)]
else:
prh = self.PlayreadyHeader.parse(box.data)
self.wrm_headers = self._read_playready_objects(prh)
except ConstructError:
if int.from_bytes(data[:2], byteorder="little") > 3:
try:
# PlayReady Header
prh = self.PlayreadyHeader.parse(data)
self.wrm_headers = self._read_playready_objects(prh)
except ConstructError:
raise InvalidPssh("Could not parse data as a PSSH Box nor a PlayReady Header")
else:
try:
# PlayReady Object
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
def _is_utf_16_le(data: bytes) -> bool:
if len(data) % 2 != 0:
return False
try:
decoded = data.decode('utf-16-le')
except UnicodeDecodeError:
return False
for char in decoded:
if not (0x20 <= ord(char) <= 0x7E):
return False
return True
@staticmethod
def _read_playready_objects(header: Container) -> List[WRMHeader]:
return list(map(
lambda pro: WRMHeader(pro.data),
filter(
lambda pro: pro.type == 1,
header.records
)
))