PSSH: Parse PlayReadyObjects efficiently, parse multiple records

The previous method was overall fine, but assumed only one PlayReadyHeader was in the PlayReadyObject. It also incorrectly assumed the start data to be garbage data when it's actually the header for the PlayReadyObject.
This commit is contained in:
rlaphoenix 2022-12-26 23:33:10 +00:00
parent 3a910bd03a
commit 61097ce6de
1 changed files with 40 additions and 20 deletions

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import base64 import base64
import binascii import binascii
import string import string
from io import BytesIO
from typing import Union, Optional from typing import Union, Optional
from uuid import UUID from uuid import UUID
@ -214,9 +215,9 @@ class PSSH:
Get all Key IDs from within the Box or Init Data, wherever possible. Get all Key IDs from within the Box or Init Data, wherever possible.
Supports: Supports:
- Version 1 Boxes - Version 1 PSSH Boxes
- Widevine Headers - WidevineCencHeaders
- PlayReady Headers (4.0.0.0->4.3.0.0) - PlayReadyHeaders (4.0.0.0->4.3.0.0)
""" """
if self.version == 1 and self.__key_ids: if self.version == 1 and self.__key_ids:
return self.__key_ids return self.__key_ids
@ -236,24 +237,43 @@ class PSSH:
] ]
if self.system_id == PSSH.SystemId.PlayReady: if self.system_id == PSSH.SystemId.PlayReady:
xml_string = self.init_data.decode("utf-16-le") # Assuming init data is a PRO (PlayReadyObject)
# some of these init data has garbage(?) in front of it # https://learn.microsoft.com/en-us/playready/specifications/playready-header-specification
xml_string = xml_string[xml_string.index("<"):] pro_data = BytesIO(self.init_data)
xml = load_xml(xml_string) pro_length = int.from_bytes(pro_data.read(4), "little")
header_version = xml.attrib["version"] if pro_length != len(self.init_data):
if header_version == "4.0.0.0": raise ValueError("The PlayReadyObject seems to be corrupt (too big or small, or missing data).")
key_ids = xml.xpath("DATA/KID/text()") pro_record_count = int.from_bytes(pro_data.read(2), "little")
elif header_version == "4.1.0.0":
key_ids = xml.xpath("DATA/PROTECTINFO/KID/@VALUE") for _ in range(pro_record_count):
elif header_version in ("4.2.0.0", "4.3.0.0"): prr_type = int.from_bytes(pro_data.read(2), "little")
key_ids = xml.xpath("DATA/PROTECTINFO/KIDS/KID/@VALUE") prr_length = int.from_bytes(pro_data.read(2), "little")
prr_value = pro_data.read(prr_length)
if prr_type != 0x01:
# No PlayReady Header, skip and hope for something else
# TODO: Add support for Embedded License Stores (0x03)
continue
prr_header = load_xml(prr_value.decode("utf-16-le"))
prr_header_version = prr_header.attrib["version"]
if prr_header_version == "4.0.0.0":
key_ids = prr_header.xpath("DATA/KID/text()")
elif prr_header_version == "4.1.0.0":
key_ids = prr_header.xpath("DATA/PROTECTINFO/KID/@VALUE")
elif prr_header_version in ("4.2.0.0", "4.3.0.0"):
# TODO: Retain the Encryption Scheme information in v4.3.0.0
# This is because some Key IDs can be AES-CTR while some are AES-CBC.
# Conversion to WidevineCencHeader could use this information.
key_ids = prr_header.xpath("DATA/PROTECTINFO/KIDS/KID/@VALUE")
else: else:
raise ValueError(f"Unsupported PlayReady header version {header_version}") raise ValueError(f"Unsupported PlayReadyHeader version {prr_header_version}")
return [ return [
UUID(bytes=base64.b64decode(key_id)) UUID(bytes=base64.b64decode(key_id))
for key_id in key_ids for key_id in key_ids
] ]
raise ValueError("Unsupported PlayReadyObject, no PlayReadyHeader within the object.")
raise ValueError(f"This PSSH is not supported by key_ids() property, {self.dumps()}") raise ValueError(f"This PSSH is not supported by key_ids() property, {self.dumps()}")
def dump(self) -> bytes: def dump(self) -> bytes: