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:
parent
3a910bd03a
commit
61097ce6de
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue