commit 20a81f11c067e5030061a09086e853285c527823 Author: titus Date: Tue Apr 1 14:17:16 2025 +0200 + Initial Commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cfe676c --- /dev/null +++ b/LICENSE @@ -0,0 +1,403 @@ +Attribution-NonCommercial-NoDerivatives 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 +International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial-NoDerivatives 4.0 International Public +License ("Public License"). To the extent this Public License may be +interpreted as a contract, You are granted the Licensed Rights in +consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the +Licensor receives from making the Licensed Material available under +these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + c. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + d. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + e. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + f. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + g. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + h. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce and reproduce, but not Share, Adapted Material + for NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material, You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + For the avoidance of doubt, You do not have permission under + this Public License to Share Adapted Material. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only and provided You do not Share Adapted Material; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..50e8acc --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# pyplayready +All of this is already public. Almost 100% of this code has been derived from the mspr_toolkit. + +## Installation +```shell +pip install pyplayready +``` + +Run `pyplayready --help` to view available cli functions + +## Devices +Run the command below to create a Playready Device (.prd) from a `bgroupcert.dat` and `zgpriv.dat`: +```shell +pyplayready create-device -c bgroupcert.dat -k zgpriv.dat +``` + +Test a playready device: +```shell +pyplayready test DEVICE.prd +``` + +> [!IMPORTANT] +> There currently isn't a proper method of extracting Group Certificates/Keys. They can be found inside older Samsung phones/Smart TVs, Windows DLLs and set-top-boxes in encrypted form. + +Export a provisioned device to its raw .dat files +```shell +pyplayready export-device DEVICE.prd +``` + +## Usage +An example code snippet: + +```python +from pyplayready.cdm import Cdm +from pyplayready.device import Device +from pyplayready.system.pssh import PSSH + +import requests + +device = Device.load("C:/Path/To/A/Device.prd") +cdm = Cdm.from_device(device) +session_id = cdm.open() + +pssh = PSSH( + "AAADfHBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAA1xcAwAAAQABAFIDPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AH" + "QAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABh" + "AHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUg" + "BPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQA" + "UgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgA0AFIAcABsAGIAKwBUAGIATgBFAFMAOAB0AE" + "cAawBOAEYAVwBUAEUASABBAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEsATABqADMAUQB6AFEAUAAvAE4AQQA9ADwALwBD" + "AEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcABzADoALwAvAHAAcgBvAGYAZgBpAGMAaQBhAGwAcwBpAHQAZQAuAGsAZQ" + "B5AGQAZQBsAGkAdgBlAHIAeQAuAG0AZQBkAGkAYQBzAGUAcgB2AGkAYwBlAHMALgB3AGkAbgBkAG8AdwBzAC4AbgBlAHQALwBQAGwAYQB5AFIA" + "ZQBhAGQAeQAvADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwASQBJAFMAXwBEAFIATQBfAF" + "YARQBSAFMASQBPAE4APgA4AC4AMQAuADIAMwAwADQALgAzADEAPAAvAEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4APAAvAEMAVQBT" + "AFQATwBNAEEAVABUAFIASQBCAFUAVABFAFMAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==" +) + +request = cdm.get_license_challenge(session_id, pssh.wrm_headers[0]) + +response = requests.post( + url="https://test.playready.microsoft.com/service/rightsmanager.asmx?cfg=(persist:false,sl:2000)", + headers={ + 'Content-Type': 'text/xml; charset=UTF-8', + }, + data=request, +) + +cdm.parse_license(session_id, response.text) + +for key in cdm.get_keys(session_id): + print(f"{key.key_id.hex}:{key.key.hex()}") + +cdm.close(session_id) +``` + +## Disclaimer + +1. This project requires a valid Microsoft Certificate and Group Key, which are not provided by this project. +2. Public test provisions are available and provided by Microsoft to use for testing projects such as this one. +3. This project does not condone piracy or any action against the terms of the DRM systems. +4. All efforts in this project have been the result of Reverse-Engineering, Publicly available research, and Trial & Error. +5. Do not use this program to decrypt or access any content for which you do not have the legal rights or explicit permission. +6. Unauthorized decryption or distribution of copyrighted materials is a violation of applicable laws and intellectual property rights. +7. This tool must not be used for any illegal activities, including but not limited to piracy, circumventing digital rights management (DRM), or unauthorized access to protected content. +8. The developers, contributors, and maintainers of this program are not responsible for any misuse or illegal activities performed using this software. +9. By using this program, you agree to comply with all applicable laws and regulations governing digital rights and copyright protections. + +## Credits ++ [mspr_toolkit](https://security-explorations.com/materials/mspr_toolkit.zip) diff --git a/pyplayready/__init__.py b/pyplayready/__init__.py new file mode 100644 index 0000000..21ba9cf --- /dev/null +++ b/pyplayready/__init__.py @@ -0,0 +1,14 @@ +from pyplayready.cdm import * +from pyplayready.crypto.ecc_key import * +from pyplayready.crypto.elgamal import * +from pyplayready.device import * +from pyplayready.license.key import * +from pyplayready.license.xml_key import * +from pyplayready.license.xmrlicense import * +from pyplayready.remote.remotecdm import * +from pyplayready.system.bcert import * +from pyplayready.system.pssh import * +from pyplayready.system.session import * + + +__version__ = "0.6.0" diff --git a/pyplayready/cdm.py b/pyplayready/cdm.py new file mode 100644 index 0000000..7b7d7a8 --- /dev/null +++ b/pyplayready/cdm.py @@ -0,0 +1,362 @@ +from __future__ import annotations + +import base64 +import time +from typing import List, Union, Optional +from uuid import UUID +import xml.etree.ElementTree as ET + +import xmltodict +from Crypto.Cipher import AES +from Crypto.Hash import SHA256 +from Crypto.Random import get_random_bytes +from Crypto.Util.Padding import pad +from Crypto.Util.strxor import strxor + +from ecpy.curves import Point, Curve + +from pyplayready.crypto import Crypto +from pyplayready.drmresults import DRMResult +from pyplayready.system.bcert import CertificateChain +from pyplayready.crypto.ecc_key import ECCKey +from pyplayready.license.key import Key +from pyplayready.license.xmrlicense import XMRLicense, XMRObjectTypes +from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, ServerException) +from pyplayready.system.session import Session +from pyplayready.system.wrmheader import WRMHeader + + +class Cdm: + MAX_NUM_OF_SESSIONS = 16 + + rgbMagicConstantZero = bytes.fromhex("7ee9ed4af773224f00b8ea7efb027cbb") + + def __init__( + self, + security_level: int, + certificate_chain: Optional[CertificateChain], + encryption_key: Optional[ECCKey], + signing_key: Optional[ECCKey], + client_version: str = "10.0.16384.10011", + ): + self.security_level = security_level + self.certificate_chain = certificate_chain + self.encryption_key = encryption_key + self.signing_key = signing_key + self.client_version = client_version + + self.__crypto = Crypto() + self._wmrm_key = Point( + x=0xc8b6af16ee941aadaa5389b4af2c10e356be42af175ef3face93254e7b0b3d9b, + y=0x982b27b5cb2341326e56aa857dbfd5c634ce2cf9ea74fca8f2af5957efeea562, + curve=Curve.get_curve("secp256r1") + ) + + self.__sessions: dict[bytes, Session] = {} + + @classmethod + def from_device(cls, device) -> Cdm: + """Initialize a Playready CDM from a Playready Device (.prd) file""" + return cls( + security_level=device.security_level, + certificate_chain=device.group_certificate, + encryption_key=device.encryption_key, + signing_key=device.signing_key + ) + + def open(self) -> bytes: + """Open a Playready Content Decryption Module (CDM) session""" + if len(self.__sessions) > self.MAX_NUM_OF_SESSIONS: + raise TooManySessions(f"Too many Sessions open ({self.MAX_NUM_OF_SESSIONS}).") + + session = Session(len(self.__sessions) + 1) + self.__sessions[session.id] = session + + return session.id + + def close(self, session_id: bytes) -> None: + """Close a Playready Content Decryption Module (CDM) session """ + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id.hex()} is invalid.") + del self.__sessions[session_id] + + def _get_key_data(self, session: Session) -> bytes: + return self.__crypto.ecc256_encrypt( + public_key=self._wmrm_key, + plaintext=session.xml_key.get_point() + ) + + def _get_cipher_data(self, session: Session) -> bytes: + b64_chain = base64.b64encode(self.certificate_chain.dumps()).decode() + body = xmltodict.unparse({ + 'Data': { + 'CertificateChains': { + 'CertificateChain': b64_chain + }, + 'Features': { + 'Feature': { + '@Name': 'AESCBC', + '#text': '""' + }, + 'REE': { + 'AESCBCS': None + } + } + } + }, full_document=False) + + cipher = AES.new( + key=session.xml_key.aes_key, + mode=AES.MODE_CBC, + iv=session.xml_key.aes_iv + ) + + ciphertext = cipher.encrypt(pad( + body.encode(), + AES.block_size + )) + + return session.xml_key.aes_iv + ciphertext + + @staticmethod + def _build_main_body(la_content: dict, signed_info: dict, signature: str, public_signing_key: str) -> dict: + return { + 'soap:Envelope': { + '@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + '@xmlns:xsd': 'http://www.w3.org/2001/XMLSchema', + '@xmlns:soap': 'http://schemas.xmlsoap.org/soap/envelope/', + 'soap:Body': { + 'AcquireLicense': { + '@xmlns': 'http://schemas.microsoft.com/DRM/2007/03/protocols', + 'challenge': { + 'Challenge': { + '@xmlns': 'http://schemas.microsoft.com/DRM/2007/03/protocols/messages', + 'LA': la_content["LA"], + 'Signature': { + 'SignedInfo': signed_info["SignedInfo"], + '@xmlns': 'http://www.w3.org/2000/09/xmldsig#', + 'SignatureValue': signature, + 'KeyInfo': { + '@xmlns': 'http://www.w3.org/2000/09/xmldsig#', + 'KeyValue': { + 'ECCKeyValue': { + 'PublicKey': public_signing_key + } + } + } + } + } + } + } + } + } + } + + def _build_digest_content( + self, + wrm_header: str, + nonce: str, + wmrm_cipher: str, + cert_cipher: str, + protocol_version: int + ) -> dict: + return { + 'LA': { + '@xmlns': 'http://schemas.microsoft.com/DRM/2007/03/protocols', + '@Id': 'SignedData', + '@xml:space': 'preserve', + 'Version': protocol_version, + 'ContentHeader': xmltodict.parse(wrm_header), + 'CLIENTINFO': { + 'CLIENTVERSION': self.client_version + }, + 'LicenseNonce': nonce, + 'ClientTime': int(time.time()), + 'EncryptedData': { + '@xmlns': 'http://www.w3.org/2001/04/xmlenc#', + '@Type': 'http://www.w3.org/2001/04/xmlenc#Element', + 'EncryptionMethod': { + '@Algorithm': 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' + }, + 'KeyInfo': { + '@xmlns': 'http://www.w3.org/2000/09/xmldsig#', + 'EncryptedKey': { + '@xmlns': 'http://www.w3.org/2001/04/xmlenc#', + 'EncryptionMethod': { + '@Algorithm': 'http://schemas.microsoft.com/DRM/2007/03/protocols#ecc256' + }, + 'KeyInfo': { + '@xmlns': 'http://www.w3.org/2000/09/xmldsig#', + 'KeyName': 'WMRMServer' + }, + 'CipherData': { + 'CipherValue': wmrm_cipher + } + } + }, + 'CipherData': { + 'CipherValue': cert_cipher + } + } + } + } + + @staticmethod + def _build_signed_info(digest_value: str) -> dict: + return { + 'SignedInfo': { + '@xmlns': 'http://www.w3.org/2000/09/xmldsig#', + 'CanonicalizationMethod': { + '@Algorithm': 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315' + }, + 'SignatureMethod': { + '@Algorithm': 'http://schemas.microsoft.com/DRM/2007/03/protocols#ecdsa-sha256' + }, + 'Reference': { + '@URI': '#SignedData', + 'DigestMethod': { + '@Algorithm': 'http://schemas.microsoft.com/DRM/2007/03/protocols#sha256' + }, + 'DigestValue': digest_value + } + } + } + + def get_license_challenge(self, session_id: bytes, wrm_header: Union[WRMHeader, str]) -> str: + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id.hex()} is invalid.") + + if isinstance(wrm_header, str): + wrm_header = WRMHeader(wrm_header) + if not isinstance(wrm_header, WRMHeader): + raise ValueError(f"Expected WRMHeader to be a {str} or {WRMHeader} not {wrm_header!r}") + + match wrm_header.version: + case WRMHeader.Version.VERSION_4_3_0_0: + protocol_version = 5 + case WRMHeader.Version.VERSION_4_2_0_0: + protocol_version = 4 + case _: + protocol_version = 1 + + session.signing_key = self.signing_key + session.encryption_key = self.encryption_key + + la_content = self._build_digest_content( + wrm_header=wrm_header.dumps(), + nonce=base64.b64encode(get_random_bytes(16)).decode(), + wmrm_cipher=base64.b64encode(self._get_key_data(session)).decode(), + cert_cipher=base64.b64encode(self._get_cipher_data(session)).decode(), + protocol_version=protocol_version + ) + la_content_xml = xmltodict.unparse(la_content, full_document=False) + + la_hash_obj = SHA256.new() + la_hash_obj.update(la_content_xml.encode()) + la_hash = la_hash_obj.digest() + + signed_info = self._build_signed_info(base64.b64encode(la_hash).decode()) + signed_info_xml = xmltodict.unparse(signed_info, full_document=False) + + signature = self.__crypto.ecc256_sign(session.signing_key, signed_info_xml.encode()) + b64_signature = base64.b64encode(signature).decode() + + b64_public_singing_key = base64.b64encode(session.signing_key.public_bytes()).decode() + + return xmltodict.unparse(self._build_main_body(la_content, signed_info, b64_signature, b64_public_singing_key)).replace('\n', '') + + @staticmethod + def _verify_encryption_key(session: Session, licence: XMRLicense) -> bool: + ecc_keys = list(licence.get_object(XMRObjectTypes.ECC_DEVICE_KEY_OBJECT)) + if not ecc_keys: + raise InvalidLicense("No ECC public key in license") + + return ecc_keys[0].key == session.encryption_key.public_bytes() + + def parse_license(self, session_id: bytes, licence: str) -> None: + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id.hex()} is invalid.") + + if not licence: + raise InvalidLicense("Cannot parse an empty licence message") + if not isinstance(licence, str): + raise InvalidLicense(f"Expected licence message to be a {str}, not {licence!r}") + if not session.encryption_key or not session.signing_key: + raise InvalidSession("Cannot parse a license message without first making a license request") + + try: + root = ET.fromstring(licence) + faults = root.findall(".//{http://schemas.xmlsoap.org/soap/envelope/}Fault") + + for fault in faults: + status_codes = fault.findall(".//StatusCode") + for status_code in status_codes: + code = DRMResult.from_code(status_code.text) + raise ServerException(f"[{status_code.text}] ({code.name}) {code.message}") + + license_elements = root.findall(".//{http://schemas.microsoft.com/DRM/2007/03/protocols}License") + + for license_element in license_elements: + parsed_licence = XMRLicense.loads(license_element.text) + + if not self._verify_encryption_key(session, parsed_licence): + raise InvalidLicense("Public encryption key does not match") + + is_scalable = bool(next(parsed_licence.get_object(XMRObjectTypes.AUX_KEY_OBJECT), None)) + + for content_key in parsed_licence.get_content_keys(): + cipher_type = Key.CipherType(content_key.cipher_type) + + if cipher_type not in (Key.CipherType.ECC_256, Key.CipherType.ECC_256_WITH_KZ, Key.CipherType.ECC_256_VIA_SYMMETRIC): + raise InvalidLicense(f"Invalid cipher type {cipher_type}") + + via_symmetric = Key.CipherType(content_key.cipher_type) == Key.CipherType.ECC_256_VIA_SYMMETRIC + + decrypted = self.__crypto.ecc256_decrypt( + private_key=session.encryption_key, + ciphertext=content_key.encrypted_key + ) + ci, ck = decrypted[:16], decrypted[16:] + + if is_scalable: + ci, ck = decrypted[::2][:16], decrypted[1::2][:16] + + if via_symmetric: + embedded_root_license = content_key.encrypted_key[:144] + embedded_leaf_license = content_key.encrypted_key[144:] + + rgb_key = strxor(ck, self.rgbMagicConstantZero) + content_key_prime = AES.new(ck, AES.MODE_ECB).encrypt(rgb_key) + + aux_key = next(parsed_licence.get_object(XMRObjectTypes.AUX_KEY_OBJECT))["auxiliary_keys"][0]["key"] + + uplink_x_key = AES.new(content_key_prime, AES.MODE_ECB).encrypt(aux_key) + secondary_key = AES.new(ck, AES.MODE_ECB).encrypt(embedded_root_license[128:]) + + embedded_leaf_license = AES.new(uplink_x_key, AES.MODE_ECB).encrypt(embedded_leaf_license) + embedded_leaf_license = AES.new(secondary_key, AES.MODE_ECB).encrypt(embedded_leaf_license) + + ci, ck = embedded_leaf_license[:16], embedded_leaf_license[16:] + + if not parsed_licence.check_signature(ci): + raise InvalidLicense("License integrity signature does not match") + + session.keys.append(Key( + key_id=UUID(bytes_le=content_key.key_id), + key_type=content_key.key_type, + cipher_type=content_key.cipher_type, + key_length=content_key.key_length, + key=ck + )) + except Exception as e: + raise InvalidLicense(f"Unable to parse license: {e}") + + def get_keys(self, session_id: bytes) -> List[Key]: + session = self.__sessions.get(session_id) + if not session: + raise InvalidSession(f"Session identifier {session_id.hex()} is invalid.") + + return session.keys diff --git a/pyplayready/crypto/__init__.py b/pyplayready/crypto/__init__.py new file mode 100644 index 0000000..4e339eb --- /dev/null +++ b/pyplayready/crypto/__init__.py @@ -0,0 +1,96 @@ +from typing import Union, Tuple + +from Crypto.Hash import SHA256 +from Crypto.Hash.SHA256 import SHA256Hash +from Crypto.PublicKey.ECC import EccKey +from Crypto.Signature import DSS +from ecpy.curves import Point, Curve + +from pyplayready.crypto.elgamal import ElGamal +from pyplayready.crypto.ecc_key import ECCKey + + +class Crypto: + def __init__(self, curve: str = "secp256r1"): + self.curve = Curve.get_curve(curve) + self.elgamal = ElGamal(self.curve) + + def ecc256_encrypt(self, public_key: Union[ECCKey, Point], plaintext: Union[Point, bytes]) -> bytes: + if isinstance(public_key, ECCKey): + public_key = public_key.get_point(self.curve) + if not isinstance(public_key, Point): + raise ValueError(f"Expecting ECCKey or Point input, got {public_key!r}") + + if isinstance(plaintext, bytes): + plaintext = Point( + x=int.from_bytes(plaintext[:32], 'big'), + y=int.from_bytes(plaintext[32:64], 'big'), + curve=self.curve + ) + if not isinstance(plaintext, Point): + raise ValueError(f"Expecting Point or Bytes input, got {plaintext!r}") + + point1, point2 = self.elgamal.encrypt( + message_point=plaintext, + public_key=public_key + ) + return b''.join([ + self.elgamal.to_bytes(point1.x), + self.elgamal.to_bytes(point1.y), + self.elgamal.to_bytes(point2.x), + self.elgamal.to_bytes(point2.y) + ]) + + def ecc256_decrypt(self, private_key: ECCKey, ciphertext: Union[Tuple[Point, Point], bytes]) -> bytes: + if isinstance(ciphertext, bytes): + ciphertext = ( + Point( + x=int.from_bytes(ciphertext[:32], 'big'), + y=int.from_bytes(ciphertext[32:64], 'big'), + curve=self.curve + ), + Point( + x=int.from_bytes(ciphertext[64:96], 'big'), + y=int.from_bytes(ciphertext[96:128], 'big'), + curve=self.curve + ) + ) + if not isinstance(ciphertext, Tuple): + raise ValueError(f"Expecting Tuple[Point, Point] or Bytes input, got {ciphertext!r}") + + decrypted = self.elgamal.decrypt(ciphertext, int(private_key.key.d)) + return self.elgamal.to_bytes(decrypted.x) + + @staticmethod + def ecc256_sign(private_key: Union[ECCKey, EccKey], data: Union[SHA256Hash, bytes]) -> bytes: + if isinstance(private_key, ECCKey): + private_key = private_key.key + if not isinstance(private_key, EccKey): + raise ValueError(f"Expecting ECCKey or EccKey input, got {private_key!r}") + + if isinstance(data, bytes): + data = SHA256.new(data) + if not isinstance(data, SHA256Hash): + raise ValueError(f"Expecting SHA256Hash or Bytes input, got {data!r}") + + signer = DSS.new(private_key, 'fips-186-3') + return signer.sign(data) + + @staticmethod + def ecc256_verify(public_key: Union[ECCKey, EccKey], data: Union[SHA256Hash, bytes], signature: bytes) -> bool: + if isinstance(public_key, ECCKey): + public_key = public_key.key + if not isinstance(public_key, EccKey): + raise ValueError(f"Expecting ECCKey or EccKey input, got {public_key!r}") + + if isinstance(data, bytes): + data = SHA256.new(data) + if not isinstance(data, SHA256Hash): + raise ValueError(f"Expecting SHA256Hash or Bytes input, got {data!r}") + + verifier = DSS.new(public_key, 'fips-186-3') + try: + verifier.verify(data, signature) + return True + except ValueError: + return False diff --git a/pyplayready/crypto/ecc_key.py b/pyplayready/crypto/ecc_key.py new file mode 100644 index 0000000..2baab46 --- /dev/null +++ b/pyplayready/crypto/ecc_key.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import base64 +from pathlib import Path +from typing import Union + +from Crypto.Hash import SHA256 +from Crypto.PublicKey import ECC +from Crypto.PublicKey.ECC import EccKey +from ecpy.curves import Curve, Point + + +class ECCKey: + """Represents a PlayReady ECC key pair""" + + def __init__(self, key: EccKey): + self.key = key + + @classmethod + def generate(cls): + """Generate a new ECC key pair""" + return cls(key=ECC.generate(curve='P-256')) + + @classmethod + def construct(cls, private_key: Union[bytes, int]): + """Construct an ECC key pair from private/public bytes/ints""" + if isinstance(private_key, bytes): + private_key = int.from_bytes(private_key, 'big') + if not isinstance(private_key, int): + raise ValueError(f"Expecting Bytes or Int input, got {private_key!r}") + + # The public is always derived from the private key; loading the other stuff won't work + key = ECC.construct( + curve='P-256', + d=private_key, + ) + + return cls(key=key) + + @classmethod + def loads(cls, data: Union[str, bytes]) -> ECCKey: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + if len(data) not in [96, 32]: + raise ValueError(f"Invalid data length. Expecting 96 or 32 bytes, got {len(data)}") + + return cls.construct(private_key=data[:32]) + + @classmethod + def load(cls, path: Union[Path, str]) -> ECCKey: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + with Path(path).open(mode="rb") as f: + return cls.loads(f.read()) + + def dumps(self, private_only=False): + if private_only: + return self.private_bytes() + return self.private_bytes() + self.public_bytes() + + def dump(self, path: Union[Path, str], private_only=False) -> None: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(self.dumps(private_only)) + + @staticmethod + def _to_bytes(n: int) -> bytes: + byte_len = (n.bit_length() + 7) // 8 + if byte_len % 2 != 0: + byte_len += 1 + return n.to_bytes(byte_len, 'big') + + def get_point(self, curve: Curve) -> Point: + return Point(self.key.pointQ.x, self.key.pointQ.y, curve) + + def private_bytes(self) -> bytes: + return self._to_bytes(int(self.key.d)) + + def private_sha256_digest(self) -> bytes: + hash_object = SHA256.new() + hash_object.update(self.private_bytes()) + return hash_object.digest() + + def public_bytes(self) -> bytes: + return self._to_bytes(int(self.key.pointQ.x)) + self._to_bytes(int(self.key.pointQ.y)) + + def public_sha256_digest(self) -> bytes: + hash_object = SHA256.new() + hash_object.update(self.public_bytes()) + return hash_object.digest() diff --git a/pyplayready/crypto/elgamal.py b/pyplayready/crypto/elgamal.py new file mode 100644 index 0000000..ed6c3db --- /dev/null +++ b/pyplayready/crypto/elgamal.py @@ -0,0 +1,42 @@ +from typing import Tuple + +from ecpy.curves import Curve, Point +import secrets + + +class ElGamal: + """ElGamal ECC utility using ecpy""" + + def __init__(self, curve: Curve): + """Initialize the utility with a given curve type ('secp256r1' for PlayReady)""" + self.curve = curve + + @staticmethod + def to_bytes(n: int) -> bytes: + byte_len = (n.bit_length() + 7) // 8 + if byte_len % 2 != 0: + byte_len += 1 + return n.to_bytes(byte_len, 'big') + + def encrypt(self, message_point: Point, public_key: Point) -> Tuple[Point, Point]: + """ + Encrypt a single point with a given public key + + Returns an encrypted point pair + """ + ephemeral_key = secrets.randbelow(self.curve.order) + point1 = ephemeral_key * self.curve.generator + point2 = message_point + (ephemeral_key * public_key) + return point1, point2 + + @staticmethod + def decrypt(encrypted: Tuple[Point, Point], private_key: int) -> Point: + """ + Decrypt and encrypted point pair with a given private key + + Returns a single decrypted point + """ + point1, point2 = encrypted + shared_secret = private_key * point1 + decrypted_message = point2 - shared_secret + return decrypted_message diff --git a/pyplayready/device/__init__.py b/pyplayready/device/__init__.py new file mode 100644 index 0000000..c4aaaf2 --- /dev/null +++ b/pyplayready/device/__init__.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import base64 +from enum import IntEnum +from pathlib import Path +from typing import Union, Any, Optional + +from pyplayready.device.structs import DeviceStructs +from pyplayready.exceptions import OutdatedDevice +from pyplayready.system.bcert import CertificateChain +from pyplayready.crypto.ecc_key import ECCKey + + +class Device: + """Represents a PlayReady Device (.prd)""" + CURRENT_VERSION = 3 + + class SecurityLevel(IntEnum): + SL150 = 150 + SL2000 = 2000 + SL3000 = 3000 + + def __init__( + self, + *_: Any, + group_key: Optional[str, bytes, None], + encryption_key: Union[str, bytes], + signing_key: Union[str, bytes], + group_certificate: Union[str, bytes], + **__: Any + ): + if isinstance(group_key, str): + group_key = base64.b64decode(group_key) + + if isinstance(encryption_key, str): + encryption_key = base64.b64decode(encryption_key) + if not isinstance(encryption_key, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {encryption_key!r}") + + if isinstance(signing_key, str): + signing_key = base64.b64decode(signing_key) + if not isinstance(signing_key, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {signing_key!r}") + + if isinstance(group_certificate, str): + group_certificate = base64.b64decode(group_certificate) + if not isinstance(group_certificate, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {group_certificate!r}") + + self.group_key = None if group_key is None else ECCKey.loads(group_key) + self.encryption_key = ECCKey.loads(encryption_key) + self.signing_key = ECCKey.loads(signing_key) + self.group_certificate = CertificateChain.loads(group_certificate) + self.security_level = self.group_certificate.get_security_level() + + @classmethod + def loads(cls, data: Union[str, bytes]) -> Device: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + parsed = DeviceStructs.prd.parse(data) + return cls(**{ + **parsed, + 'group_key': parsed.get('group_key', None) + }) + + @classmethod + def load(cls, path: Union[Path, str]) -> Device: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + with Path(path).open(mode="rb") as f: + return cls.loads(f.read()) + + def dumps(self) -> bytes: + if not self.group_key: + raise OutdatedDevice("Cannot dump a v2 device, re-create it or use a Device with a version of 3 or higher") + + return DeviceStructs.prd.build(dict( + version=self.CURRENT_VERSION, + group_key=self.group_key.dumps(), + encryption_key=self.encryption_key.dumps(), + signing_key=self.signing_key.dumps(), + group_certificate_length=len(self.group_certificate.dumps()), + group_certificate=self.group_certificate.dumps(), + )) + + def dump(self, path: Union[Path, str]) -> None: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(self.dumps()) + + def get_name(self) -> str: + name = f"{self.group_certificate.get_name()}_sl{self.group_certificate.get_security_level()}" + return ''.join(char for char in name if (char.isalnum() or char in '_- ')).strip().lower().replace(" ", "_") diff --git a/pyplayready/device/structs.py b/pyplayready/device/structs.py new file mode 100644 index 0000000..9ef07de --- /dev/null +++ b/pyplayready/device/structs.py @@ -0,0 +1,41 @@ +from construct import Struct, Const, Int8ub, Bytes, this, Int32ub, Switch, Embedded + + +class DeviceStructs: + magic = Const(b"PRD") + + # was never in production + v1 = Struct( + "group_key_length" / Int32ub, + "group_key" / Bytes(this.group_key_length), + "group_certificate_length" / Int32ub, + "group_certificate" / Bytes(this.group_certificate_length) + ) + + v2 = Struct( + "group_certificate_length" / Int32ub, + "group_certificate" / Bytes(this.group_certificate_length), + "encryption_key" / Bytes(96), + "signing_key" / Bytes(96) + ) + + v3 = Struct( + "group_key" / Bytes(96), + "encryption_key" / Bytes(96), + "signing_key" / Bytes(96), + "group_certificate_length" / Int32ub, + "group_certificate" / Bytes(this.group_certificate_length) + ) + + prd = Struct( + "signature" / magic, + "version" / Int8ub, + Embedded(Switch( + lambda ctx: ctx.version, + { + 1: v1, + 2: v2, + 3: v3 + } + )) + ) diff --git a/pyplayready/drmresults.py b/pyplayready/drmresults.py new file mode 100644 index 0000000..2a4feca --- /dev/null +++ b/pyplayready/drmresults.py @@ -0,0 +1,908 @@ +from enum import Enum + + +class DRMResult(Enum): + """Holds Playready DRM results (error codes and their messages)""" + + DRM_SUCCESS = (0x00000000, "Operation was successful.") + DRM_S_FALSE = (0x00000001, "Operation was successful, but returned a FALSE test condition.") + DRM_S_MORE_DATA = (0x00000002, "Operation was successful, but more data is available.") + DRM_S_TEST_SKIP_FILE = (0x0004C300, "Skip processing this file, not an eror.") + DRM_S_TEST_CONVERTED_FILE = (0x0004C301, "The file was converted to a PlayReady file during the action.") + DRM_E_OEM_CONSTRAINT_1_FAIL = (0x8004DFF, "Failed while OEM CONSTRAINT-1 check") + DRM_E_OEM_CONSTRAINT_2_FAIL = (0x8004DFF, "Failed while OEM CONSTRAINT-2 check") + DRM_E_OEM_CONSTRAINT_3_FAIL = (0x8004DFF, "Failed while OEM CONSTRAINT-3 check") + DRM_E_HDCP_NON_SECURE = (0x8004DFF, "Attempt to play back through insecure HDMI output path") + DRM_E_OUTOFMEMORY = (0x80000002, "Insufficient resources exist to complete the request.") + DRM_E_NOTIMPL = (0x80004001, "The requested operation is not implemented.") + DRM_E_POINTER = (0x80004003, "Invalid pointer.") + DRM_E_FAIL = (0x80004005, "The requested operation failed.") + DRM_E_HLOS_TAMPERED = (0x80004006, "HLOS tampered.") + DRM_E_OPL_BLOCKED = (0x80004007, "OPL blocked.") + DRM_E_LOAD_IMG = (0x80004008, "Failed on loading playready image.") + DRM_E_VER_MISMATCH = (0x80004009, "Version mismatch between HLOS and TZ.") + DRM_E_SET_BANDWIDTH = (0x8000400A, "Failed on setting bandwidth.") + DRM_E_OUT_OF_BOUND = (0x8000400B, "Out of bound.") + DRM_E_PLAY_ENABLER_BLOCKED = (0x8000400C, "WFD is blocked as play enabler object for HDCP doesn't exist in license") + DRM_E_HDMI_READ_FAIL = (0x8000400E, "Failed to read HDMI Status") + DRM_E_FILENOTFOUND = (0x80030002, "A requested file could not be found.") + DRM_E_FILEOPEN = (0x8003006E, "A request failed due to a file being open.") + DRM_E_VERIFICATION_FAILURE = (0x80040E80, "Validation of a Longhorn certificate failed.") + DRM_E_RSA_SIGNATURE_ERROR = (0x80040E82, "Error in RSA(PSS) signature.") + DRM_E_BAD_RSA_EXPONENT = (0x80040E86, "An incorrect RSA exponent was supplied for a public key.") + DRM_E_P256_CONVERSION_FAILURE = (0x80040E87, "An error occurred while converting between P256 types.") + DRM_E_P256_PKCRYPTO_FAILURE = (0x80040E88, "An error occurred in an asymmetric P256 cryptographic operation.") + DRM_E_P256_PLAINTEXT_MAPPING_FAILURE = (0x80040E89, "An error occurred while attempting to map a plaintext array to a EC Point: There is no conversion for this byte array to a EC Point.") + DRM_E_P256_INVALID_SIGNATURE = (0x80040E8A, "The ECDSA signature to be verified was not a valid signature format.") + DRM_E_P256_ECDSA_VERIFICATION_ERROR = (0x80040E8B, "The ECDSA verification algorithm encountered an unknown error.") + DRM_E_P256_ECDSA_SIGNING_ERROR = (0x80040E8C, "The ECDSA signature algorithm encountered an unknown error.") + DRM_E_P256_HMAC_KEYGEN_FAILURE = (0x80040E8D, "Could not generate a valid HMAC key under constraint where CK || HMACK is a valid x coord on the EC (P256).") + DRM_E_CH_VERSION_MISSING = (0x80041103, "Missing content header version.") + DRM_E_CH_KID_MISSING = (0x80041104, "Missing KID attribute in content header.") + DRM_E_CH_LAINFO_MISSING = (0x80041105, "Missing LAINFO attribute in content header.") + DRM_E_CH_CHECKSUM_MISSING = (0x80041106, "Missing content header checksum.") + DRM_E_CH_ATTR_MISSING = (0x80041107, "Missing content header attribute.") + DRM_E_CH_INVALID_HEADER = (0x80041108, "Invalid content header.") + DRM_E_CH_INVALID_CHECKSUM = (0x80041109, "Invalid checksum in the header.") + DRM_E_CH_UNABLE_TO_VERIFY = (0x8004110A, "Unable to verify signature of content header.") + DRM_E_CH_UNSUPPORTED_VERSION = (0x8004110B, "Unsupported content header version.") + DRM_E_CH_UNSUPPORTED_HASH_ALGORITHM = (0x8004110C, "Unsupported hash algorithm.") + DRM_E_CH_UNSUPPORTED_SIGN_ALGORITHM = (0x8004110D, "Unsupported signature algorithm.") + DRM_E_CH_BAD_KEY = (0x8004110E, "Invalid key.") + DRM_E_CH_INCOMPATIBLE_HEADER_TYPE = (0x8004110F, "Incompatible content header type.") + DRM_E_HEADER_ALREADY_SET = (0x80041110, "Content header type is already set. Reinitialize is required.") + DRM_E_CH_MULTIPLE_KIDS = (0x80041111, "Content header includes multiple KIDs. The operation requested is unsupported.") + DRM_E_CH_NOT_SIGNED = (0x80041113, "The header was not signed.") + DRM_E_CH_UNKNOWN_ERROR = (0x80041116, "Unknown Error.") + DRM_E_CDMIGRATIONTOOL_INVALID_FILE = (0x80041180, "File cannot be migrated because it is invalid.") + DRM_E_CDMIGRATIONTOOL_FILE_IS_NOT_CD_RIPPED = (0x80041181, "File cannot be migrated because it was not ripped from CD.") + DRM_E_CDMIGRATIONTOOL_FILE_IS_NOT_PROTECTED = (0x80041182, "File cannot be migrated because it is not protected.") + DRM_E_CDMIGRATIONTOOL_LICENSE_KID_INVALID = (0x80041183, "File cannot be migrated because the server returned a license with an invalid KID.") + DRM_E_CDMIGRATIONTOOL_LICENSE_KID_MISMATCH = (0x80041184, "File cannot be migrated because the server returned a license with a KID that did not match the content.") + DRM_E_CDMIGRATIONTOOL_LICENSE_CONTENT_KEY_INVALID = (0x80041185, "File cannot be migrated because the server returned a license with an invalid content key.") + DRM_E_CDMIGRATIONTOOL_INVALID_ASF_FORMAT = (0x80041186, "File cannot be migrated because the ASF is corrupt.") + DRM_E_CDMIGRATIONTOOL_INVALID_ASF_PACKETS = (0x80041187, "File cannot be migrated because the ASF packets are corrupt.") + DRM_E_CDMIGRATIONTOOL_CONTENT_KEY_CACHE_CORRUPT = (0x80041188, "File cannot be migrated because the content key obtained from the local cache is invalid.") + DRM_E_CDMIGRATIONTOOL_FILE_WRITE_ERROR = (0x80041189, "File cannot be migrated because the file could not be written.") + DRM_E_CDMIGRATIONTOOL_CANCELLED = (0x8004118A, "File migration was cancelled.") + LIC_INIT_FAILURE = (0x80041201, "LIC_INIT_FAILURE") + LIC_LICENSE_NOTSET = (0x80041202, "LIC_LICENSE_NOTSET") + LIC_PARAM_NOT_OPTIONAL = (0x80041203, "LIC_PARAM_NOT_OPTIONAL") + LIC_MEMORY_ALLOCATION_ERROR = (0x80041204, "LIC_MEMORY_ALLOCATION_ERROR") + LIC_INVALID_LICENSE = (0x80041205, "LIC_INVALID_LICENSE") + LIC_FIELD_MISSING = (0x80041206, "LIC_FIELD_MISSING") + DRM_E_LIC_UNSUPPORTED_VALUE = (0x80041207, " DRM_E_LIC_UNSUPPORTED_VALUE") + LIC_UNKNOWN_ERROR = (0x80041208, "LIC_UNKNOWN_ERROR") + LIC_INVALID_REVLIST = (0x80041209, "LIC_INVALID_REVLIST") + LIC_EXPIRED_CERT = (0x8004120A, "LIC_EXPIRED_CERT") + DRM_E_CDMI_INVALID_INITIALIZATION_DATA = (0x80041301, "Invalid initialization data.") + DRM_E_CDMI_PERSISTENT_LICENSE_FOR_NON_PERSISTENT_LICENSE_SESSION = (0x80041302, "A persistent license was provided for a session that was not persistent-license.") + DRM_E_CDMI_SECURE_STOP_LICENSE_FOR_NON_PERSISTENT_USAGE_RECORD_SESSION = (0x80041303, "A secure stop license was provided for a session that was not persistent-usage-record.") + DRM_E_CDMI_TEMPORARY_LICENSE_FOR_NON_TEMPORARY_SESSION = (0x80041304, "An in-memory-only license without secure-stop was provided for a session that was not temporary.") + DRM_E_CDMI_UNSUPPORTED_KEY_SYSTEM = (0x80041305, "The requested key system is not supported by PlayReady.") + DRM_E_CDMI_UNSUPPORTED_INITIALIZATION_DATA_TYPES = (0x80041306, "None of the requested initialization data types are supported by PlayReady.") + DRM_E_CDMI_UNSUPPORTED_DISTINCTIVE_IDENTIFIER = (0x80041307, "The requested distinctive identifier setting is not supported by PlayReady.") + DRM_E_CDMI_UNSUPPORTED_SESSION_TYPE = (0x80041308, "The requested session type is not supported by PlayReady.") + DRM_E_CDMI_UNSUPPORTED_INITIALIZATION_DATA = (0x80041309, "The provided initialization data is not supported by PlayReady.") + DRM_E_CDMI_SESSION_ALREADY_USED = (0x8004130A, "The session has already been used.") + DRM_E_CDMI_SESSION_UNINITIALIZED = (0x8004130B, "The session is not yet initialized.") + DRM_E_CDMI_SESSION_CLOSED = (0x8004130C, "The session is closed.") + DRM_E_CDMI_SESSION_ID_NOT_FOUND = (0x8004130D, "The given session ID could not be found.") + DRM_E_CDMI_SESSION_TYPE_MISMATCH = (0x8004130E, "The given session was initialized with a different session type than the session being loaded or Load/Remove was called on a temporary session.") + DRM_E_CDMI_SECURE_STOP_LICENSE_FOR_NON_PERSISTENT_USAGE_RECORD_SESSION_2 = (0x8004130F, "A secure stop license was provided for a session that was not persistent-usage-record.") + DRM_E_CPRMEXP_NOERROR = (0x80041400, "DRM_E_CPRMEXP_NOERROR") # actually the code for DRM_E_CPRMEXP_BASECODE + CPRMEXP_PARAM_NOT_OPTIONAL = (0x80041401, "CPRMEXP_PARAM_NOT_OPTIONAL") + CPRMEXP_MEMORY_ALLOCATION_ERROR = (0x80041402, "CPRMEXP_MEMORY_ALLOCATION_ERROR") + CPRMEXP_NO_OPERANDS_IN_EXPRESSION = (0x80041403, "CPRMEXP_NO_OPERANDS_IN_EXPRESSION") + CPRMEXP_INVALID_TOKEN = (0x80041404, "CPRMEXP_INVALID_TOKEN") + CPRMEXP_INVALID_CONSTANT = (0x80041405, "CPRMEXP_INVALID_CONSTANT") + CPRMEXP_INVALID_VARIABLE = (0x80041406, "CPRMEXP_INVALID_VARIABLE") + CPRMEXP_INVALID_FUNCTION = (0x80041407, "CPRMEXP_INVALID_FUNCTION") + CPRMEXP_INVALID_ARGUMENT = (0x80041408, "CPRMEXP_INVALID_ARGUMENT") + CPRMEXP_INVALID_CONTEXT = (0x80041409, "CPRMEXP_INVALID_CONTEXT") + CPRMEXP_ENDOFBUFFER = (0x8004140A, "CPRMEXP_ENDOFBUFFER") + CPRMEXP_MISSING_OPERAND = (0x8004140B, "CPRMEXP_MISSING_OPERAND") + CPRMEXP_OVERFLOW = (0x8004140C, "CPRMEXP_OVERFLOW") + CPRMEXP_UNDERFLOW = (0x8004140D, "CPRMEXP_UNDERFLOW") + CPRMEXP_INCORRECT_NUM_ARGS = (0x8004140E, "CPRMEXP_INCORRECT_NUM_ARGS") + CPRMEXP_VARIABLE_EXPECTED = (0x8004140F, "CPRMEXP_VARIABLE_EXPECTED") + CPRMEXP_RETRIEVAL_FAILURE = (0x80041410, "CPRMEXP_RETRIEVAL_FAILURE") + CPRMEXP_UPDATE_FAILURE = (0x80041411, "CPRMEXP_UPDATE_FAILURE") + CPRMEXP_STRING_UNTERMINATED = (0x80041412, "CPRMEXP_STRING_UNTERMINATED") + CPRMEXP_UPDATE_UNSUPPORTED = (0x80041413, "CPRMEXP_UPDATE_UNSUPPORTED") + CPRMEXP_ISOLATED_OPERAND_OR_OPERATOR = (0x80041414, "CPRMEXP_ISOLATED_OPERAND_OR_OPERATOR") + CPRMEXP_UNMATCHED = (0x80041415, "CPRMEXP_UNMATCHED") + CPRMEXP_WRONG_TYPE_OPERAND = (0x80041416, "CPRMEXP_WRONG_TYPE_OPERAND") + CPRMEXP_TOO_MANY_OPERANDS = (0x80041417, "CPRMEXP_TOO_MANY_OPERANDS") + CPRMEXP_UNKNOWN_PARSE_ERROR = (0x80041418, "CPRMEXP_UNKNOWN_PARSE_ERROR") + CPRMEXP_UNSUPPORTED_FUNCTION = (0x80041419, "CPRMEXP_UNSUPPORTED_FUNCTION") + CPRMEXP_CLOCK_REQUIRED = (0x8004141A, "CPRMEXP_CLOCK_REQUIRED") + DRM_E_LIC_KEY_DECODE_FAILURE = (0x80048007, "Key decode failure.") + DRM_E_LIC_SIGNATURE_FAILURE = (0x80048008, "License signature failure.") + DRM_E_LIC_KEY_AND_CERT_MISMATCH = (0x80048013, "Key and cert mismatch.") + DRM_E_KEY_MISMATCH = (0x80048014, "A public/private keypair is mismatched.") + DRM_E_INVALID_SIGNATURE = (0x800480CF, "License signature failure.") + DRM_E_SYNC_ENTRYNOTFOUND = (0x800480D0, "An entry was not found in the sync store.") + DRM_E_STACKTOOSMALL = (0x800480D1, "The stack supplied to the DRM API was too small.") + DRM_E_CIPHER_NOT_INITIALIZED = (0x800480D2, "The DRM Cipher routines were not correctly initialized before calling encryption/decryption routines.") + DRM_E_DECRYPT_NOT_INITIALIZED = (0x800480D3, "The DRM decrypt routines were not correctly initialized before trying to decrypt data.") + DRM_E_SECURESTORE_LOCK_NOT_OBTAINED = (0x800480D4, "Before reading or writing data to securestore in raw mode, first the lock must be obtained using DRM_SST_OpenData.") + DRM_E_PKCRYPTO_FAILURE = (0x800480D5, "An error occurred in an asymmetric cryptographic operation.") + DRM_E_INVALID_DST_SLOT_SIZE = (0x800480D6, "Invalid DST slot size is specified.") + DRM_E_UNSUPPORTED_VERSION = (0x80049005, " DRM_E_UNSUPPORTED_VERSION") + DRMUTIL_EXPIRED_CERT = (0x80049006, "DRMUTIL_EXPIRED_CERT") + DRMUTIL_INVALID_CERT = (0x80049007, "DRMUTIL_INVALID_CERT") + DRM_E_DEVICE_NOT_REGISTERED = (0x8004A000, "The DEVICEID does not exist in the device store") + DRM_E_TOO_MANY_INCLUSION_GUIDS = (0x8004A001, "The license contained more than DRM_MAX_INCLUSION_GUIDS entries in its inclusion list") + DRM_E_REVOCATION_GUID_NOT_RECOGNIZED = (0x8004A002, "The revocation list type GUID was not recognized") + DRM_E_LIC_CHAIN_TOO_DEEP = (0x8004A003, "The license chained deeper than this implementation can handle") + DRM_E_DEVICE_SECURITY_LEVEL_TOO_LOW = (0x8004A004, "The security level of the remote device is too low to receive the license") + DRM_E_DST_BLOCK_CACHE_CORRUPT = (0x8004A005, "The block header cache returned invalid data") + DRM_E_CONTRACT_FAILED = (0x8004A006, "The error code returned by the API is not present in the contract") + DRM_E_DST_BLOCK_CACHE_MISS = (0x8004A007, "The block header cache didn't contain the requested block header") + DRM_E_INVALID_METERRESPONSE_SIGNATURE = (0x8004A013, "Invalid signature in meter response") + DRM_E_INVALID_LICENSE_REVOCATION_LIST_SIGNATURE = (0x8004A014, "Invalid signature in license revocation list.") + DRM_E_INVALID_METERCERT_SIGNATURE = (0x8004A015, "Invalid signature in metering certificate") + DRM_E_METERSTORE_DATA_NOT_FOUND = (0x8004A016, "Metering data slot not found due to bad data in response file") + DRM_E_NO_LICENSES_TO_SYNC = (0x8004A017, "No more licenses to sync") + DRM_E_INVALID_REVOCATION_LIST = (0x8004A018, "The revocation list version does not match the current revocation version") + DRM_E_ENVELOPE_CORRUPT = (0x8004A019, "The envelope archive or file is corrupt") + DRM_E_ENVELOPE_FILE_NOT_COMPATIBLE = (0x8004A01A, "The envelope file is not compatible with this version of the porting kit") + DRM_E_EXTENDED_RESTRICTION_NOT_UNDERSTOOD = (0x8004A01B, "An extensible restriction was not understood by the app, and is mark as being required") + DRM_E_INVALID_SLK = (0x8004A01C, "An ILA SLK (symmetric session key) was found, but did not contain valid data") + DRM_E_DEVCERT_MODEL_MISMATCH = (0x8004A01D, "The model string in the certificate does not match the model of the device and so the cert must be re-generated.") + DRM_E_OUTDATED_REVOCATION_LIST = (0x8004A01E, "The revocation list is outdated. It is required for the revocation list to be refreshed at least every 90 days.") + DRM_E_DSTR_NOT_FOUND = (0x8004A01F, "The substring search inside a DRM string failed.") + DRM_E_DEVICE_NOT_INITIALIZED = (0x8004C001, "This device has not been initialized against a DRM init service") + DRM_E_DRM_NOT_INITIALIZED = (0x8004C002, "The app has not call DRM_Init properly") + DRM_E_INVALIDRIGHT = (0x8004C003, "A right in the license in invalid") + DRM_E_INCOMPATABLELICENSESIZE = (0x8004C004, "The size of the license is incompatable. DRM doesn't understand this license") + DRM_E_INVALIDLICENSEFLAGS = (0x8004C005, "The flags in the license are invalid. DRM either doesn't understand them or they are conflicting") + DRM_E_INVALID_LICENSE = (0x8004C006, "The license is invalid") + DRM_E_CONDITIONFAIL = (0x8004C007, "A condition in the license failed to pass") + DRM_E_CONDITIONNOTSUPPORTED = (0x8004C008, "A condition in the license is not supported by this verison of DRM") + DRM_E_LICENSE_EXPIRED = (0x8004C009, "The license has expired either by depleting a play count or via an end time.") + DRM_E_LICENSENOTYETVALID = (0x8004C00A, "The license start time had not come to pass yet.") + DRM_E_RIGHTS_NOT_AVAILABLE = (0x8004C00B, "The rights the app has requested are not available in the license") + DRM_E_LICENSEMISMATCH = (0x8004C00C, "The license content id/ sku id doesn't match that requested by the app") + DRM_E_WRONG_PARAMETER_TYPE = (0x8004C00D, "The token parameter was of an incompatible type.") # renamed from DRM_E_WRONG_TOKEN_TYPE so it doesn't conflict with a duplicate that has a different code + DRM_E_NORIGHTSREQUESTED = (0x8004C00E, "The app has not requested any rights before trying to bind") + DRM_E_LICENSE_NOT_BOUND = (0x8004C00F, "A license has not been bound to. Decrypt can not happen without a successful bind call") + DRM_E_HASH_MISMATCH = (0x8004C010, "A Keyed Hash check failed.") + DRM_E_INVALIDTIME = (0x8004C011, "A time structure is invalid.") + DRM_E_LICENSESTORENOTFOUND = (0x8004C012, "The external license store was not found.") + DRM_E_LICENSE_NOT_FOUND = (0x8004C013, "A license was not found in the license store.") + DRM_E_LICENSE_VERSION_NOT_SUPPORTED = (0x8004C014, "The DRM license version is not supported by the DRM version on the device.") + DRM_E_INVALIDBINDID = (0x8004C015, "The bind id is invalid.") + DRM_E_UNSUPPORTED_ALGORITHM = (0x8004C016, "The encryption algorithm required for this operation is not supported.") + DRM_E_ALGORITHMNOTSET = (0x8004C017, "The encryption algorithm required for this operation is not supported.") + DRM_E_LICENSESERVERNEEDSKEY = (0x8004C018, "The license server needs a version of the device bind key from the init service.") + DRM_E_INVALID_LICENSE_STORE = (0x8004C019, "The license store version number is incorrect, or the store is invalid in some other way.") + DRM_E_FILE_READ_ERROR = (0x8004C01A, "There was an error reading a file.") + DRM_E_FILE_WRITE_ERROR = (0x8004C01B, "There was an error writing a file.") + DRM_E_CLIENTTIMEINVALID = (0x8004C01C, "The time/clock on the device is not in sync with the license server within tolerance.") + DRM_E_DST_STORE_FULL = (0x8004C01D, "The data store is full.") + DRM_E_NO_XML_OPEN_TAG = (0x8004C01E, "XML open tag not found") + DRM_E_NO_XML_CLOSE_TAG = (0x8004C01F, "XML close tag not found") + DRM_E_INVALID_XML_TAG = (0x8004C020, "Invalid XML tag") + DRM_E_NO_XML_CDATA = (0x8004C021, "No XML CDATA found") + DRM_E_DSTNAMESPACEFULL = (0x8004C022, "No more room for DST Namespace") + DRM_E_DST_NAMESPACE_NOT_FOUND = (0x8004C023, "No DST Namespace found") + DRM_E_DST_SLOT_NOT_FOUND = (0x8004C024, "DST Dataslot not found") + DRM_E_DST_SLOT_EXISTS = (0x8004C025, "DST Dataslot already exists") + DRM_E_DST_CORRUPTED = (0x8004C026, "The data store is corrupted") + DRM_E_DST_SEEK_ERROR = (0x8004C027, "There was an error attempting to seek in the Data Store") + DRM_E_DSTNAMESPACEINUSE = (0x8004C028, "No DST Namespace in use") + DRM_E_INVALID_SECURESTORE_PASSWORD = (0x8004C029, "The password used to open the secure store key was not able to validate the secure store hash.") + DRM_E_SECURESTORE_CORRUPT = (0x8004C02A, "The secure store is corrupt") + DRM_E_SECURESTORE_FULL = (0x8004C02B, "The current secure store key is full. No more data can be added.") + DRM_E_NOACTIONINLICENSEREQUEST = (0x8004C02C, "No action(s) added for license request") + DRM_E_DUPLICATED_HEADER_ATTRIBUTE = (0x8004C02D, "Duplicated attribute in Header") + DRM_E_NO_KID_IN_HEADER = (0x8004C02E, "No KID attribute in Header") + DRM_E_NO_LAINFO_IN_HEADER = (0x8004C02F, "No LAINFO attribute in Header") + DRM_E_NO_CHECKSUM_IN_HEADER = (0x8004C030, "No Checksum attribute in Header") + DRM_E_DST_BLOCK_MISMATCH = (0x8004C031, "DST block mismatch") + DRM_E_BACKUP_EXISTS = (0x8004C032, "Backup file already exist.") + DRM_E_LICENSE_TOOLONG = (0x8004C033, "License size is too long") + DRM_E_DST_EXISTS = (0x8004C034, "A DST already exists in the specified location") + DRM_E_INVALID_DEVICE_CERTIFICATE = (0x8004C035, "The device certificate is invalid.") + DRM_E_DST_LOCK_FAILED = (0x8004C036, "Locking a segment of the DST failed.") + DRM_E_FILE_SEEK_ERROR = (0x8004C037, "File Seek Error") + DRM_E_DST_NOT_LOCKED_EXCLUSIVE = (0x8004C038, "Existing lock is not exclusive") + DRM_E_DST_EXCLUSIVE_LOCK_ONLY = (0x8004C039, "Only exclusive lock is accepted") + DRM_E_DSTRESERVEDKEYDETECTED = (0x8004C03A, "DST reserved key value detected in UniqueKey") + DRM_E_V1_NOT_SUPPORTED = (0x8004C03B, "V1 Lic Acquisition is not supported") + DRM_E_HEADER_NOT_SET = (0x8004C03C, "Content header is not set") + DRM_E_NEEDDEVCERTINDIV = (0x8004C03D, "The device certificate is template. It need Devcert Indiv") + DRM_E_MACHINE_ID_MISMATCH = (0x8004C03E, "The device has Machine Id different from that in devcert.") + DRM_E_CLK_INVALID_RESPONSE = (0x8004C03F, "The secure clock response is invalid.") + DRM_E_CLK_INVALID_DATE = (0x8004C040, "The secure clock response is invalid.") + DRM_E_CLK_UNSUPPORTED_VALUE = (0x8004C041, "The secure clock response has unsupported value.") + DRM_E_INVALIDDEVICECERTIFICATETEMPLATE = (0x8004C042, "The device certificate is invalid.") + DRM_E_DEVCERT_EXCEEDS_SIZE_LIMIT = (0x8004C043, "The device certificate exceeds max size") + DRM_E_DEVCERTTEMPLATEEXCEEDSSIZELIMIT = (0x8004C044, "The device certificate template exceeds max size") + DRM_E_DEVCERTREADERROR = (0x8004C045, "Can't get the device certificate") + DRM_E_DEVCERTWRITEERROR = (0x8004C046, "Can't store the device certificate") + DRM_E_PRIVKEY_READ_ERROR = (0x8004C047, "Can't get device private key") + DRM_E_PRIVKEYWRITEERROR = (0x8004C048, "Can't store device private key") + DRM_E_DEVCERT_TEMPLATE_READ_ERROR = (0x8004C049, "Can't get the device certificate template") + DRM_E_CLK_NOT_SUPPORTED = (0x8004C04A, "The secure clock is not supported.") + DRM_E_DEVCERTINDIV_NOT_SUPPORTED = (0x8004C04B, "The Devcert Indiv is not supported.") + DRM_E_METERING_NOT_SUPPORTED = (0x8004C04C, "The Metering is not supported.") + DRM_E_CLK_RESETSTATEREADERROR = (0x8004C04D, "Can not read Secure clock Reset State.") + DRM_E_CLK_RESETSTATEWRITEERROR = (0x8004C04E, "Can not write Secure clock Reset State.") + DRM_E_XMLNOTFOUND = (0x8004C04F, "a required XML tag was not found") + DRM_E_METERING_WRONG_TID = (0x8004C050, "wrong TID sent on metering response") + DRM_E_METERING_INVALID_COMMAND = (0x8004C051, "wrong command sent on metering response") + DRM_E_METERING_STORE_CORRUPT = (0x8004C052, "The metering store is corrupt") + DRM_E_CERTIFICATE_REVOKED = (0x8004C053, "A certificate given to DRM was revoked.") + DRM_E_CRYPTO_FAILED = (0x8004C054, "A cryptographic operation failed.") + DRM_E_STACK_CORRUPT = (0x8004C055, "The stack allocator context is corrupt. Likely a buffer overrun problem.") + DRM_E_UNKNOWN_BINDING_KEY = (0x8004C056, "A matching binding key could not be found for the license.") + DRM_E_V1_LICENSE_CHAIN_NOT_SUPPORTED = (0x8004C057, "License chaining with V1 content is not supported.") + DRM_E_WRONG_TOKEN_TYPE = (0x8004C058, "The wrong type of token was used.") + DRM_E_POLICY_METERING_DISABLED = (0x8004C059, "Metering code was called but metering is disabled by group or user policy") + DRM_E_POLICY_ONLINE_DISABLED = (0x8004C05A, "online communication is disabled by group policy") + DRM_E_CLK_NOT_SET = (0x8004C05B, "Time based licenses can not be used because the secure clock is not set on the device.") + DRM_E_NO_CLK_SUPPORTED = (0x8004C05C, "Time based licenses can not be used because the device does not support any clock.") + DRM_E_NO_URL = (0x8004C05D, "Can not find URL info.") + DRM_E_UNKNOWN_DEVICE_PROPERTY = (0x8004C05E, "Unknown device property.") + DRM_E_METERING_MID_MISMATCH = (0x8004C05F, "The metering ID is not same in Metering Cert and metering response data") + DRM_E_METERING_RESPONSE_DECRYPT_FAILED = (0x8004C060, "The encrypted section of metering response can not be decrypted") + DRM_E_RIV_TOO_SMALL = (0x8004C063, "RIV on the machine is too small.") + DRM_E_STACK_ALREADY_INITIALIZED = (0x8004C064, "DRM_STK_Init called for initialized stack") + DRM_E_DEVCERT_REVOKED = (0x8004C065, "The device certificate given to DRM is revoked.") + DRM_E_OEM_RSA_DECRYPTION_ERROR = (0x8004C066, "Error in OEM RSA Decryption.") + DRM_E_INVALID_DEVSTORE_ATTRIBUTE = (0x8004C067, "Invalid device attributes in the device store") + DRM_E_INVALID_DEVSTORE_ENTRY = (0x8004C068, "The device store data entry is corrupted") + DRM_E_OEM_RSA_ENCRYPTION_ERROR = (0x8004C069, "Error in OEM RSA Encryption process") + DRM_E_DST_NAMESPACE_EXISTS = (0x8004C06A, "The DST Namespace already exists.") + DRM_E_PERF_SCOPING_ERROR = (0x8004C06B, "Error in performance scope context") + DRM_E_PRECISION_ARITHMETIC_FAIL = (0x8004C06C, "Operation involving multiple precision arithmetic fails") + DRM_E_OEM_RSA_INVALID_PRIVATE_KEY = (0x8004C06D, "Invalid private key.") + DRM_E_NO_OPL_CALLBACK = (0x8004C06E, "There is no callback function to process the output restrictions specified in the license") + DRM_E_INVALID_PLAYREADY_OBJECT = (0x8004C06F, "Structure of PlayReady object is invalid") + DRM_E_DUPLICATE_LICENSE = (0x8004C070, "There is already a license in the store with the same KID & LID") + DRM_E_REVOCATION_NOT_SUPPORTED = (0x8004C071, "Device does not support revocation, while revocation data was placed into license policy structure.") + DRM_E_RECORD_NOT_FOUND = (0x8004C072, "Record with requested type was not found in PlayReady object.") + DRM_E_BUFFER_BOUNDS_EXCEEDED = (0x8004C073, "An array is being referenced outside of it's bounds.") + DRM_E_INVALID_BASE64 = (0x8004C074, "An input string contains invalid Base64 characters.") + DRM_E_PROTOCOL_VERSION_NOT_SUPPORTED = (0x8004C075, "The protocol version is not supported.") + DRM_E_INVALID_LICENSE_RESPONSE_SIGNATURE = (0x8004C076, "Cannot verify license acquisition's response because signature is invalid.") + DRM_E_INVALID_LICENSE_RESPONSE_ID = (0x8004C077, "Cannot verify license acquisition's response because response ID is invalid.") + DRM_E_LICENSE_RESPONSE_SIGNATURE_MISSING = (0x8004C078, "Cannot verify license acquisition's response because either response ID, license nonce or signature is missing.") + DRM_E_INVALID_DOMAIN_JOIN_RESPONSE_SIGNATURE = (0x8004C079, "Cannot verify domain join response because signature is invalid.") + DRM_E_DOMAIN_JOIN_RESPONSE_SIGNATURE_MISSING = (0x8004C07A, "Cannot verify domain join response because either signing certificate chain or signature is missing.") + DRM_E_ACTIVATION_REQUIRED = (0x8004C07B, "The device must be activated before initialization can succeed.") + DRM_E_ACTIVATION_INTERNAL_ERROR = (0x8004C07C, "A server error occurred during device activation.") + DRM_E_ACTIVATION_GROUP_CERT_REVOKED_ERROR = (0x8004C07D, "The activation group cert has been revoked and the application must be updated with a new client lib.") + DRM_E_ACTIVATION_NEW_CLIENT_LIB_REQUIRED_ERROR = (0x8004C07E, "The client lib used by the application is not supported and must be updated.") + DRM_E_ACTIVATION_BAD_REQUEST = (0x8004C07F, "The activation request is invalid") + DRM_E_FILEIO_ERROR = (0x8004C080, "Encountered a system error during file I/O.") + DRM_E_DISKSPACE_ERROR = (0x8004C081, "Out of disk space for storing playready files.") + DRM_E_UPLINK_LICENSE_NOT_FOUND = (0x8004C082, "A license was found in the license store but no license was found for its uplink ID.") + DRM_E_ACTIVATION_CLIENT_ALREADY_CURRENT = (0x8004C083, "The activation client already has the lastest verion.") + DRM_E_LICENSE_REALTIME_EXPIRED = (0x8004C084, "The license has expired during decryption due to the RealTimeExpiration Restriction.") + DRM_E_DECRYPTOR_CANNOT_CLONE = (0x8004C085, "The decryptor cannot be cloned due to restrictions in the corresponding license.") + DRM_E_ACTIVATION_REQUIRED_REACTIVATION_POSSIBLE = (0x8004C086, "The device must be activated or reactivated before initialization can succeed.") + DRM_E_LRB_NOLGPUBKEY = (0x8004C0A0, "LRB does not contain a valid LGPUBKEY.") + DRM_E_LRB_INVALIDSIGNATURE = (0x8004C0A1, "Signature inside LRB is invalid.") + DRM_E_LRB_LGPUBKEY_MISMATCH = (0x8004C0A2, "LRB is signed with a pubkey different from LGPUBKEY") + DRM_E_LRB_INVALIDLICENSEDATA = (0x8004C0A3, "LRB is signed with a pubkey different from LGPUBKEY") + DRM_E_LICEVAL_LICENSE_NOT_SUPPLIED = (0x8004C0C0, "License not supplied in the liceval context") + DRM_E_LICEVAL_KID_MISMATCH = (0x8004C0C1, "Mismatch between KID from header and the one inside license") + DRM_E_LICEVAL_LICENSE_REVOKED = (0x8004C0C2, "License for this content has been revoked") + DRM_E_LICEVAL_UPDATE_FAILURE = (0x8004C0C3, "Failed to update content revocation") + DRM_E_LICEVAL_REQUIRED_REVOCATION_LIST_NOT_AVAILABLE = (0x8004C0C4, "Failed to update content revocation") + DRM_E_LICEVAL_INVALID_PRND_LICENSE = (0x8004C0C5, "License is an invalid PRND license. PRND license cannot have metering ID, expire-after-first-play or domain properties.") + DRM_E_XMR_OBJECT_ALREADY_EXISTS = (0x8004C0E0, "XMR builder context already has this object.") + DRM_E_XMR_OBJECT_NOTFOUND = (0x8004C0E1, "XMR object was not found.") + DRM_E_XMR_REQUIRED_OBJECT_MISSING = (0x8004C0E2, "XMR license doesn't have one or more required objects.") + DRM_E_XMR_INVALID_UNKNOWN_OBJECT = (0x8004C0E3, "Invalid unknown object") + DRM_E_XMR_LICENSE_BINDABLE = (0x8004C0E4, "XMR license does not contain the Cannot Bind right") + DRM_E_XMR_LICENSE_NOT_BINDABLE = (0x8004C0E5, "XMR license cannot be bound to because of the Cannot Bind right") + DRM_E_XMR_UNSUPPORTED_XMR_VERSION = (0x8004C0E6, "The version of XMR license is not supported for the current action") + DRM_E_NOT_CRL_BLOB = (0x8004C100, "CRL blob provided for parsing does not start with CBLB. It means file is not CRL blob at all.") + DRM_E_BAD_CRL_BLOB = (0x8004C101, "The file is structured as CRL blob, but there is some error in file structure or one of CRLs inside is invalid.") + DRM_E_INVALID_DEVCERT_ATTRIBUTE = (0x8004C200, "The attributes in the Device certificate are invalid") + DRM_E_TEST_PKCRYPTO_FAILURE = (0x8004C300, "Error in PK encryption/decryption crypto test cases.") + DRM_E_TEST_PKSIGN_VERIFY_ERROR = (0x8004C301, "Digital signature verification failed.") + DRM_E_TEST_ENCRYPT_ERROR = (0x8004C302, "Error in encryption of cipher text.") + DRM_E_TEST_RC4KEY_FAILED = (0x8004C303, "RC4 key failed during crypto operations.") + DRM_E_TEST_DECRYPT_ERROR = (0x8004C304, "Error in cipher text decryption.") + DRM_E_TEST_DESKEY_FAILED = (0x8004C305, "Decrypted data not equal to original data in a DES operation.") + DRM_E_TEST_CBC_INVERSEMAC_FAILURE = (0x8004C306, "Decrypted data not equal to original in Inverse MAC operation.") + DRM_E_TEST_HMAC_FAILURE = (0x8004C307, "Error in hashed data in HMAC operation.") + DRM_E_TEST_INVALIDARG = (0x8004C308, "Error in the number of arguments or argument data in Test files.") + DRM_E_TEST_DEVICE_PRIVATE_KEY_INCORRECTLY_STORED = (0x8004C30A, "DRMManager context should not contain the device private key.") + DRM_E_TEST_DRMMANAGER_CONTEXT_NULL = (0x8004C30B, "DRMManager context is NULL.") + DRM_E_TEST_UNEXPECTED_REVINFO_RESULT = (0x8004C30C, "Revocation cache result was not as expected.") + DRM_E_TEST_RIV_MISMATCH = (0x8004C30D, "Revocation Info Version(RIV) mismatch.") + DRM_E_TEST_URL_ERROR = (0x8004C310, "There is an error in the URL from the challenge generated.") + DRM_E_TEST_MID_MISMATCH = (0x8004C311, "The MIDs returned from the DRM_MANAGER_CONTEXT does not match the test input.") + DRM_E_TEST_METER_CERTIFICATE_MISMATCH = (0x8004C312, "The input data does not match with the Metering certificate returned from the license.") + DRM_E_TEST_LICENSE_STATE_MISMATCH = (0x8004C313, "The input data and license state returned from the license do not match.") + DRM_E_TEST_SOURCE_ID_MISMATCH = (0x8004C316, "The input data and license state returned from the license do not match.") + DRM_E_TEST_UNEXPECTED_LICENSE_COUNT = (0x8004C317, "The input data and the number of license from the KID do not match.") + DRM_E_TEST_UNEXPECTED_DEVICE_PROPERTY = (0x8004C318, "Unknown device property.") + DRM_E_TEST_DRMMANAGER_MISALIGNED_BYTES = (0x8004C319, "Error due to misalignment of bytes.") + DRM_E_TEST_LICENSE_RESPONSE_ERROR = (0x8004C31A, "The license response callbacks did not provide the expected data.") + DRM_E_TEST_OPL_MISMATCH = (0x8004C31B, "The minimum levels of the compressed/uncompressed Digital and Analog Video do not match the OPL.") + DRM_E_TEST_INVALID_OPL_CALLBACK = (0x8004C31C, "The callback type supplied is not valid.") + DRM_E_TEST_INCOMPLETE = (0x8004C31D, "The test function failed to complete.") + DRM_E_TEST_UNEXPECTED_OUTPUT = (0x8004C31E, "The output of the function being tested does not match the expected output.") + DRM_E_TEST_DLA_NO_CONTENT_HEADER = (0x8004C31F, "Content Header Information was not retrieved correctly in DLA Sync Tests.") + DRM_E_TEST_DLA_CONTENT_HEADER_FOUND = (0x8004C320, "Content Header Information was found when it should not have been in DLA Sync Tests.") + DRM_E_TEST_SYNC_LSD_INCORRECT = (0x8004C321, "DRM_SNC_GetSyncStoreEntry returned incorrect License State Data.") + DRM_E_TEST_TOO_SLOW = (0x8004C322, "The performance test failed because DRM took longer than its maximum time.") + DRM_E_TEST_LICENSESTORE_NOT_OPEN = (0x8004C323, "The License Store contexts in the App Manager context are not open.") + DRM_E_TEST_DEVICE_NOT_INITED = (0x8004C324, "The device instance has not been initialized prior to use.") + DRM_E_TEST_VARIABLE_NOT_SET = (0x8004C325, "A global variable needed for test execution has not been set correctly.") + DRM_E_TEST_NOMORE = (0x8004C326, "The same as DRM_E_NOMORE, only explicitly used in test code.") + DRM_E_TEST_FILE_LOAD_ERROR = (0x8004C327, "There was an error loading a test data file.") + DRM_E_TEST_LICENSE_ACQ_FAILED = (0x8004C328, "The attempt to acquire a license failed.") + DRM_E_TEST_UNSUPPORTED_FILE_FORMAT = (0x8004C329, "A file format is being used which is not supported by the test function.") + DRM_E_TEST_PARSING_ERROR = (0x8004C32A, "There was an error parsing input parameter.") + DRM_E_TEST_NOTIMPL = (0x8004C32B, "The specified test API is not implemented.") + DRM_E_TEST_VARIABLE_NOTFOUND = (0x8004C32C, "The specified test varaible was not found in the shared variable table.") + DRM_E_TEST_VARIABLE_LISTFULL = (0x8004C32D, "The shared test variable table is full.") + DRM_E_TEST_UNEXPECTED_CONTENT_PROPERTY = (0x8004C32E, "Unknown content property.") + DRM_E_TEST_PRO_HEADER_NOT_SET = (0x8004C32F, "PlayReady Object Header not set.") + DRM_E_TEST_NON_PRO_HEADER_TYPE = (0x8004C330, "Incompatible header - PlayReady Object Header expected.") + DRM_E_TEST_INVALID_DEVICE_WRAPPER = (0x8004C331, "The Device Simulator Device Wrapper is not valid.") + DRM_E_TEST_INVALID_WMDM_WRAPPER = (0x8004C332, "The Device Simulator WMDM Wrapper is not valid.") + DRM_E_TEST_INVALID_WPD_WRAPPER = (0x8004C333, "The Device Simulator WPD Wrapper is not valid.") + DRM_E_TEST_INVALID_FILE = (0x8004C334, "The data file given was invalid.") + DRM_E_TEST_PROPERTY_NOT_FOUND = (0x8004C335, "The object did not have the property which was queried.") + DRM_E_TEST_METERING_DATA_INCORRECT = (0x8004C336, "The metering data reported is incorrect.") + DRM_E_TEST_FILE_ALREADY_OPEN = (0x8004C337, "The handle variable for a test file is not NULL. This indicates that a file was opened and not closed properly.") + DRM_E_TEST_FILE_NOT_OPEN = (0x8004C338, "The handle variable for a test file is NULL. This indicates that a file was not opened.") + DRM_E_TEST_PICT_COLUMN_TOO_WIDE = (0x8004C339, "The PICT input file contains a column which is too wide for the test parser to handle.") + DRM_E_TEST_PICT_COLUMN_MISMATCH = (0x8004C33A, "The PICT input file contains a row which doesn't have the same number of columns as the header row.") + DRM_E_TEST_TUX_TEST_SKIPPED = (0x8004C33B, "TUX cannot find the speficied test case in target dll. Test Skipped.") + DRM_E_TEST_KEYFILE_VERIFICATION_FAILURE = (0x8004C33C, "Verification of the Keyfile context failed.") + DRM_E_TEST_DATA_VERIFICATION_FAILURE = (0x8004C33D, "Data does not match expected value and failed verification.") + DRM_E_TEST_NET_FAIL = (0x8004C33E, "The Test failed to perform Network I/O.") + DRM_E_TEST_CLEANUP_FAIL = (0x8004C33F, "A failure occurred during the test case cleanup phase.") + DRM_E_TEST_LICGEN_UNSUPPORTED_VALUE = (0x8004C340, "A property used during license generation is not supported.") + DRM_E_LOGICERR = (0x8004C3E8, "DRM code has a logic error in it. This result should never be returned. There is an unhandled code path if it is returned.") + DRM_E_INVALID_REV_INFO = (0x8004C3E9, "The rev info blob is invalid.") + DRM_E_SYNCLISTNOTSUPPORTED = (0x8004C3EA, "The device does not support synclist.") + DRM_E_REVOCATION_BUFFER_TOO_SMALL = (0x8004C3EB, "The revocation buffer is too small.") + DRM_E_DEVICE_ALREADY_REGISTERED = (0x8004C3EC, "There exists already a device in the device store with the same DEVICEID that was given.") + DRM_E_DST_NOT_COMPATIBLE = (0x8004C3ED, "The data store version is incompatible with this version of DRM.") + DRM_E_RSA_DECRYPTION_ERROR = (0x8004C3F0, "The data block/Encoded message used in OAEP decoding is incorrect.") + DRM_E_OEM_RSA_MESSAGE_TOO_BIG = (0x8004C3F1, "The base message buffer is larger than the given modulus.") + DRM_E_METERCERT_NOT_FOUND = (0x8004C3F2, "The metering certificate was not found in the store.") + DRM_E_MODULAR_ARITHMETIC_FAILURE = (0x8004C3F3, "A failure occurred in bignum modular arithmetic.") + DRM_E_FEATURE_NOT_SUPPORTED = (0x8004C3F4, "The feature is not supported in this release.") + DRM_E_REVOCATION_INVALID_PACKAGE = (0x8004C3F5, "The revocation package is invalid") + DRM_E_HWID_ERROR = (0x8004C3F6, "Failed to get the hardware ID.") + DRM_E_VAR_NOT_INITIALIZED = (0x8004C3F7, "Variable was not initialized.") + DRM_E_DOMAIN_INVALID_GUID = (0x8004C500, "Not a correct GUID.") + DRM_E_DOMAIN_INVALID_CUSTOM_DATA_TYPE = (0x8004C501, "Not a valid custom data type.") + DRM_E_DOMAIN_STORE_ADD_DATA = (0x8004C502, "Failed to add data into the domain store.") + DRM_E_DOMAIN_STORE_GET_DATA = (0x8004C503, "Failed to retrieve data from the domain store.") + DRM_E_DOMAIN_STORE_DELETE_DATA = (0x8004C504, "Failed to delete data from the domain store.") + DRM_E_DOMAIN_STORE_OPEN_STORE = (0x8004C505, "Failed to open the domain store.") + DRM_E_DOMAIN_STORE_CLOSE_STORE = (0x8004C506, "Failed to close the domain store.") + DRM_E_DOMAIN_BIND_LICENSE = (0x8004C507, "Failed to bind to the domain license.") + DRM_E_DOMAIN_INVALID_CUSTOM_DATA = (0x8004C508, "Not a valid custom data.") + DRM_E_DOMAIN_NOT_FOUND = (0x8004C509, "No domain information is found.") + DRM_E_DOMAIN_INVALID_DOMKEYXMR_DATA = (0x8004C50A, "The domain join response contains invalid domain privkey XMR data.") + DRM_E_DOMAIN_STORE_INVALID_KEY_RECORD = (0x8004C50B, "Invalid format of domain private key record read from the domain store.") + DRM_E_DOMAIN_JOIN_TOO_MANY_KEYS = (0x8004C50C, "The server returned too many domain keys for the client to handle.") + DRM_E_DEVICE_DOMAIN_JOIN_REQUIRED = (0x8004C580, "This error code communicates to the application that the device is not a member of a domain. The app can uses this error code in turn to decide whether it needs to join the domain or not") + DRM_E_SERVER_INTERNAL_ERROR = (0x8004C600, "An internal server error occurred.") + DRM_E_SERVER_INVALID_MESSAGE = (0x8004C601, "The message sent to the server was invalid.") + DRM_E_SERVER_DEVICE_LIMIT_REACHED = (0x8004C602, "The device limit for the domain has been reached.") + DRM_E_SERVER_INDIV_REQUIRED = (0x8004C603, "Individualization of the client is required.") + DRM_E_SERVER_SERVICE_SPECIFIC = (0x8004C604, "An error specific to the service has occurred.") + DRM_E_SERVER_DOMAIN_REQUIRED = (0x8004C605, "A Domain certificate is required.") + DRM_E_SERVER_RENEW_DOMAIN = (0x8004C606, "The Domain certificate needs to be renewed.") + DRM_E_SERVER_UNKNOWN_METERINGID = (0x8004C607, "The metering identifier is unknown.") + DRM_E_SERVER_COMPUTER_LIMIT_REACHED = (0x8004C608, "The computer limit for the domain has been reached.") + DRM_E_SERVER_PROTOCOL_FALLBACK = (0x8004C609, "The client should fallback to the V2 license acquisition protocol.") + DRM_E_SERVER_NOT_A_MEMBER = (0x8004C60A, "The client was removed from the domain in an offline fashion and thus still has a domain cert, but not a valid domain membership.") + DRM_E_SERVER_PROTOCOL_VERSION_MISMATCH = (0x8004C60B, "The protocol version specified was not supported by the server.") + DRM_E_SERVER_UNKNOWN_ACCOUNTID = (0x8004C60C, "The account identifier is unknown.") + DRM_E_SERVER_PROTOCOL_REDIRECT = (0x8004C60D, "The protocol has a redirect.") + DRM_E_SERVER_UNKNOWN_TRANSACTIONID = (0x8004C610, "The transaction identifier is unknown.") + DRM_E_SERVER_INVALID_LICENSEID = (0x8004C611, "The license identifier is invalid.") + DRM_E_SERVER_MAXIMUM_LICENSEID_EXCEEDED = (0x8004C612, "The maximum number of license identifiers in the request was exceeded.") + DRM_E_LICACQ_TOO_MANY_LICENSES = (0x8004C700, "There are too many licenses in the license response.") + DRM_E_LICACQ_ACK_TRANSACTION_ID_TOO_BIG = (0x8004C701, "The Transaction ID specified by the server exceeds the allocated buffer.") + DRM_E_LICACQ_ACK_MESSAGE_NOT_CREATED = (0x8004C702, "The license acquisition acknowledgement message could not be created.") + DRM_E_INITIATORS_UNKNOWN_TYPE = (0x8004C780, "The initiator type is unknown.") + DRM_E_INITIATORS_INVALID_SERVICEID = (0x8004C781, "The service ID data is not valid.") + DRM_E_INITIATORS_INVALID_ACCOUNTID = (0x8004C782, "The account ID data is not valid.") + DRM_E_INITIATORS_INVALID_MID = (0x8004C783, "The account ID data is not valid.") + DRM_E_INITIATORS_MISSING_DC_URL = (0x8004C784, "Domain Controller URL is missing.") + DRM_E_INITIATORS_MISSING_CONTENT_HEADER = (0x8004C785, "Content header is missing.") + DRM_E_INITIATORS_MISSING_LAURL_IN_CONTENT_HEADER = (0x8004C786, "Missing license acquisition URL in content header.") + DRM_E_INITIATORS_MISSING_METERCERT_URL = (0x8004C787, "Meter certificate server URL is missing.") + DRM_E_BCERT_INVALID_SIGNATURE_TYPE = (0x8004C800, "An invalid signature type was encountered") + DRM_E_BCERT_CHAIN_TOO_DEEP = (0x8004C801, "There are, or there would be, too many certificates in the certificate chain") + DRM_E_BCERT_INVALID_CERT_TYPE = (0x8004C802, "An invalid certificate type was encountered") + DRM_E_BCERT_INVALID_FEATURE = (0x8004C803, "An invalid feature entry was encountered OR the porting kit was linked with mutually incompatible features or features incompatible with the certificate") + DRM_E_BCERT_INVALID_KEY_USAGE = (0x8004C804, "An invalid public key usage was encountered") + DRM_E_BCERT_INVALID_SECURITY_VERSION = (0x8004C805, "An invalid Indiv Box security version was encountered") + DRM_E_BCERT_INVALID_KEY_TYPE = (0x8004C806, "An invalid public key type was encountered") + DRM_E_BCERT_INVALID_KEY_LENGTH = (0x8004C807, "An invalid public key length was encountered") + DRM_E_BCERT_INVALID_MAX_LICENSE_SIZE = (0x8004C808, "An invalid maximum license size value was encountered") + DRM_E_BCERT_INVALID_MAX_HEADER_SIZE = (0x8004C809, "An invalid maximum license header size value was encountered") + DRM_E_BCERT_INVALID_MAX_LICENSE_CHAIN_DEPTH = (0x8004C80A, "An invalid maximum license chain depth was encountered") + DRM_E_BCERT_INVALID_SECURITY_LEVEL = (0x8004C80B, "An invalid security level was encountered") + DRM_E_BCERT_PRIVATE_KEY_NOT_SPECIFIED = (0x8004C80C, "A private key for signing the certificate was not provided to the builder") + DRM_E_BCERT_ISSUER_KEY_NOT_SPECIFIED = (0x8004C80D, "An issuer key was not provided to the builder") + DRM_E_BCERT_ACCOUNT_ID_NOT_SPECIFIED = (0x8004C80E, "An account ID was not provided to the builder") + DRM_E_BCERT_SERVICE_ID_NOT_SPECIFIED = (0x8004C80F, "A service provider ID was not provided to the builder") + DRM_E_BCERT_CLIENT_ID_NOT_SPECIFIED = (0x8004C810, "A client ID was not provided to the builder") + DRM_E_BCERT_DOMAIN_URL_NOT_SPECIFIED = (0x8004C811, "A domain URL was not provided to the builder") + DRM_E_BCERT_DOMAIN_URL_TOO_LONG = (0x8004C812, "The domain URL contains too many ASCII characters") + DRM_E_BCERT_HARDWARE_ID_NOT_SPECIFIED = (0x8004C813, "A hardware ID was not provided to the builder") + DRM_E_BCERT_HARDWARE_ID_TOO_LONG = (0x8004C814, "A hardware ID is longer than the maximum supported bytes") + DRM_E_BCERT_SERIAL_NUM_NOT_SPECIFIED = (0x8004C815, "A device serial number was not provided to the builder") + DRM_E_BCERT_CERT_ID_NOT_SPECIFIED = (0x8004C816, "A certificate ID was not provided to the builder") + DRM_E_BCERT_PUBLIC_KEY_NOT_SPECIFIED = (0x8004C817, "A public key for the certificate was not provided to the builder or not found by the parser") + DRM_E_BCERT_KEY_USAGES_NOT_SPECIFIED = (0x8004C818, "The public key usage information was not provided to the builder or not found by the parser") + DRM_E_BCERT_STRING_NOT_NULL_TERMINATED = (0x8004C819, "Data string is not null-teminated") + DRM_E_BCERT_OBJECTHEADER_LEN_TOO_BIG = (0x8004C81A, "Object length in object header is too big") + DRM_E_BCERT_INVALID_ISSUERKEY_LENGTH = (0x8004C81B, "IssuerKey Length value is invalid") + DRM_E_BCERT_BASICINFO_CERT_EXPIRED = (0x8004C81C, "Certificate is expired") + DRM_E_BCERT_UNEXPECTED_OBJECT_HEADER = (0x8004C81D, "Object header has unexpected values") + DRM_E_BCERT_ISSUERKEY_KEYINFO_MISMATCH = (0x8004C81E, "The cert's Issuer Key does not match key info in the next cert") + DRM_E_BCERT_INVALID_MAX_KEY_USAGES = (0x8004C81F, "Number of key usage entries is invalid") + DRM_E_BCERT_INVALID_MAX_FEATURES = (0x8004C820, "Number of features is invalid") + DRM_E_BCERT_INVALID_CHAIN_HEADER_TAG = (0x8004C821, "Cert chain header tag is invalid") + DRM_E_BCERT_INVALID_CHAIN_VERSION = (0x8004C822, "Cert chain version is invalid") + DRM_E_BCERT_INVALID_CHAIN_LENGTH = (0x8004C823, "Cert chain length value is invalid") + DRM_E_BCERT_INVALID_CERT_HEADER_TAG = (0x8004C824, "Cert header tag is invalid") + DRM_E_BCERT_INVALID_CERT_VERSION = (0x8004C825, "Cert version is invalid") + DRM_E_BCERT_INVALID_CERT_LENGTH = (0x8004C826, "Cert length value is invalid") + DRM_E_BCERT_INVALID_SIGNEDCERT_LENGTH = (0x8004C827, "Length of signed portion of certificate is invalid") + DRM_E_BCERT_INVALID_PLATFORM_IDENTIFIER = (0x8004C828, "An invalid Platform Identifier was specified") + DRM_E_BCERT_INVALID_NUMBER_EXTDATARECORDS = (0x8004C829, "An invalid number of extended data records") + DRM_E_BCERT_INVALID_EXTDATARECORD = (0x8004C82A, "An invalid extended data record") + DRM_E_BCERT_EXTDATA_LENGTH_MUST_PRESENT = (0x8004C82B, "Extended data record length must be present.") + DRM_E_BCERT_EXTDATA_PRIVKEY_MUST_PRESENT = (0x8004C82C, "Extended data record length must be present.") + DRM_E_BCERT_INVALID_EXTDATA_LENGTH = (0x8004C82D, "Calculated and written extended data object lengths do not match.") + DRM_E_BCERT_EXTDATA_IS_NOT_PROVIDED = (0x8004C82E, "Extended data is not provided, the cert builder cannot write it.") + DRM_E_BCERT_HWIDINFO_IS_MISSING = (0x8004C82F, "The PC certificate is correct but is not ready to use because has no HWID information") + DRM_E_BCERT_INVALID_EXTDATA_SIGNED_LENGTH = (0x8004C830, "Length of signed portion of extended data info is invalid") + DRM_E_BCERT_INVALID_EXTDATA_RECORD_TYPE = (0x8004C831, "Extended data record type is invalid") + DRM_E_BCERT_EXTDATAFLAG_CERT_TYPE_MISMATCH = (0x8004C832, "Certificate of this type cannot have extended data flag set") + DRM_E_BCERT_METERING_ID_NOT_SPECIFIED = (0x8004C833, "An metering ID was not provided to the builder") + DRM_E_BCERT_METERING_URL_NOT_SPECIFIED = (0x8004C834, "A metering URL was not provided to the builder") + DRM_E_BCERT_METERING_URL_TOO_LONG = (0x8004C835, "The metering URL contains too many ASCII characters") + DRM_E_BCERT_VERIFICATION_ERRORS = (0x8004C836, "Verification errors are found while parsing cert chain") + DRM_E_BCERT_REQUIRED_KEYUSAGE_MISSING = (0x8004C837, "Required key usage is missing") + DRM_E_BCERT_NO_PUBKEY_WITH_REQUESTED_KEYUSAGE = (0x8004C838, "The certificate does not contain a public key with the requested key usage") + DRM_E_BCERT_MANUFACTURER_STRING_TOO_LONG = (0x8004C839, "The manufacturer string is too long") + DRM_E_BCERT_TOO_MANY_PUBLIC_KEYS = (0x8004C83A, "There are too many public keys in the certificate") + DRM_E_BCERT_OBJECTHEADER_LEN_TOO_SMALL = (0x8004C83B, "Object length in object header is too small") + DRM_E_BCERT_INVALID_WARNING_DAYS = (0x8004C83C, "An invalid server certificate expiration warning days. Warning days must be greater than zero.") + DRM_E_BCERT_INVALID_DIGEST = (0x8004C83D, "The certificate digest is invalid.") + DRM_E_BCERT_MANUFACTURING_INFO_REQUIRED = (0x8004C83E, "This certificate type requires Manufacturer Name, Model Name, and Model Number to be set.") + DRM_E_XMLSIG_ECDSA_VERIFY_FAILURE = (0x8004C900, "Error in ECDSA signature verification.") + DRM_E_XMLSIG_SHA_VERIFY_FAILURE = (0x8004C901, "Error in SHA verification.") + DRM_E_XMLSIG_FORMAT = (0x8004C902, "The format of XML signature or encryption segment is incorrect.") + DRM_E_XMLSIG_PUBLIC_KEY_ID = (0x8004C903, "Invalud pre-shared public key ID.") + DRM_E_XMLSIG_INVALID_KEY_FORMAT = (0x8004C904, "Invalid type of public/private key format.") + DRM_E_XMLSIG_SHA_HASH_SIZE = (0x8004C905, "Size of hash is unexpected.") + DRM_E_XMLSIG_ECDSA_SIGNATURE_SIZE = (0x8004C906, "Size of ECDSA signature is unexpected.") + DRM_E_UTF_UNEXPECTED_END = (0x8004CA00, "Unexpected end of data in the middle of multibyte character.") + DRM_E_UTF_INVALID_CODE = (0x8004CA01, "UTF character maps into a code with invalid value.") + DRM_E_SOAPXML_INVALID_STATUS_CODE = (0x8004CB00, "Status code contained in the server error response is invalid.") + DRM_E_SOAPXML_XML_FORMAT = (0x8004CB01, "Cannot parse out expected XML node.") + DRM_E_SOAPXML_WRONG_MESSAGE_TYPE = (0x8004CB02, "The message type associated with the soap message is wrong.") + DRM_E_SOAPXML_SIGNATURE_MISSING = (0x8004CB03, "The message did not have a signature and needed one") + DRM_E_SOAPXML_PROTOCOL_NOT_SUPPORTED = (0x8004CB04, "The requested protocol is not supported by the DRM SOAP parser.") + DRM_E_SOAPXML_DATA_NOT_FOUND = (0x8004CB05, "The requested data is not found in the response.") + DRM_E_CRYPTO_PUBLIC_KEY_NOT_MATCH = (0x8004CC00, "The public key associated with an encrypted domain private from the server does not match any public key on the device.") + DRM_E_UNABLE_TO_RESOLVE_LOCATION_TREE = (0x8004CC01, "Unable to derive the key. May be due to blackout or no rights to the service, etc.") + DRM_E_SECURE_TRACE_BAD_GLOBAL_DATA_POINTER = (0x8004CD00, "The secure trace global data pointer is NULL") + DRM_E_SECURE_TRACE_INVALID_GLOBAL_DATA = (0x8004CD01, "The secure trace global data structure is invalid") + DRM_E_SECURE_TRACE_FORMATTING_ERROR = (0x8004CD02, "An error occured in formatting the trace message") + DRM_E_SECURE_TRACE_BAD_SCHEME_DATA_POINTER = (0x8004CD03, "A secure trace scheme data pointer is NULL") + DRM_E_SECURE_TRACE_BAD_PER_THREAD_AES_DATA_POINTER = (0x8004CD04, "The secure trace per thread AES data pointer is NULL") + DRM_E_SECURE_TRACE_BAD_PER_THREAD_AES_BUFFER_POINTER = (0x8004CD05, "A secure trace per thread AES buffer pointer is NULL") + DRM_E_SECURE_TRACE_AES_INSUFFICIENT_BUFFER = (0x8004CD06, "There is no space left in the secure trace AES buffer") + DRM_E_SECURE_TRACE_VERSION_MISMATCH = (0x8004CD07, "All drm dlls do not agree on the same secure trace version") + DRM_E_SECURE_TRACE_UNEXPECTED_ERROR = (0x8004CD08, "An expected error was encountered in secure tracing system") + DRM_E_TEE_INVALID_KEY_DATA = (0x8004CD10, "The key data given to the TEE was invalid.") + DRM_E_TEE_PROVISIONING_REQUIRED = (0x8004CD11, "Provisioning is required.") + DRM_E_TEE_INVALID_HWDRM_STATE = (0x8004CD12, "The HWDRM state is invalid, e.g. the TEE context is invalid. Reinitialization is required.") + DRM_E_TEE_PROVISIONING_REQUEST_EXPIRED = (0x8004CD13, "Provisioning request expired.") + DRM_E_TEE_CLOCK_NOT_SET = (0x8004CD14, "The TEE secure clock needs to be reset.") + DRM_E_TEE_BLOB_ACCESS_DENIED = (0x8004CD15, "The blob data is protected and cannot be transfered outside of the TEE.") + DRM_E_TEE_PROVISIONING_BAD_NONCE = (0x8004CD16, "Malformed nonce") + DRM_E_TEE_PROVISIONING_NONCE_MISMATCH = (0x8004CD17, "Nonce mismatch. Possibly another request has happened in parallel.") + DRM_E_TEE_ROOT_KEY_CHANGED = (0x8004CD18, "The root-most TEE key has changed without maintaining key history. All TEE-bound data is now invalid.") + DRM_E_TEE_PROVISIONING_INVALID_RESPONSE = (0x8004CD19, "Invalid provisioning response.") + DRM_E_TEE_PROXY_INVALID_SERIALIZATION_MESSAGE = (0x8004CD1A, "Invalid TEE proxy serialization message.") + DRM_E_TEE_PROXY_INVALID_SERIALIZATION_TYPE = (0x8004CD1B, "Invalid TEE proxy serialization type.") + DRM_E_TEE_LAYER_UNINITIALIZED = (0x8004CD1C, "TEE Layer is not initialized.") + DRM_E_TEE_INVALID_HEADER_FOOTER_SIZE = (0x8004CD1D, "The OEM defined TEE message header/footer size was not a multiple of 8 bytes.") + DRM_E_TEE_MESSAGE_TOO_LARGE = (0x8004CD1E, "TEE method invocation message is too large.") + DRM_E_TEE_CLOCK_DRIFTED = (0x8004CD1F, "TEE clock drift detected.") + DRM_E_TEE_PROXY_INVALID_BUFFER_ALIGNMENT = (0x8004CD20, "The TEE serialization buffer is incorrectly aligned. It requires 8-byte alignment.") + DRM_E_TEE_PROXY_INVALID_ALIGNMENT = (0x8004CD21, "The TEE serialization buffer has parameters that are not properly aligned.") + DRM_E_TEE_OUTPUT_PROTECTION_REQUIREMENTS_NOT_MET = (0x8004CD22, "The TEE has detected that certain output requirements are not being satisfied. Most commonly HDCP is required but not enabled on all available outputs.") + DRM_E_ND_MUST_REVALIDATE = (0x8004CE00, "The client must be revalidated before executing the intended operation.") + DRM_E_ND_INVALID_MESSAGE = (0x8004CE01, "A received message is garbled.") + DRM_E_ND_INVALID_MESSAGE_TYPE = (0x8004CE02, "A received message contains an invalid message type.") + DRM_E_ND_INVALID_MESSAGE_VERSION = (0x8004CE03, "A received message contains an invalid message version.") + DRM_E_ND_INVALID_SESSION = (0x8004CE04, "The requested session is invalid.") + DRM_E_ND_MEDIA_SESSION_LIMIT_REACHED = (0x8004CE05, "A new session cannot be opened because the maximum number of sessions has already been opened.") + DRM_E_ND_UNABLE_TO_VERIFY_PROXIMITY = (0x8004CE06, "The proximity detection procedure could not confirm that the receiver is near the transmitter in the network.") + DRM_E_ND_INVALID_PROXIMITY_RESPONSE = (0x8004CE07, "The response to the proximity detection challenge is invalid.") + DRM_E_ND_DEVICE_LIMIT_REACHED = (0x8004CE08, "The maximum number of devices in use has been reached. Unable to open additional devices.") + DRM_E_ND_BAD_REQUEST = (0x8004CE09, "The message format is invalid.") + DRM_E_ND_FAILED_SEEK = (0x8004CE0A, "It is not possible to seek to the specified mark-in point.") + DRM_E_ND_INVALID_CONTEXT = (0x8004CE0B, "Manager context or at least one of it's children is missing (or corrupt).") + DRM_E_ASF_BAD_ASF_HEADER = (0x8004CF00, "The ASF file has a bad ASF header.") + DRM_E_ASF_BAD_PACKET_HEADER = (0x8004CF01, "The ASF file has a bad packet header.") + DRM_E_ASF_BAD_PAYLOAD_HEADER = (0x8004CF02, "The ASF file has a bad payload header.") + DRM_E_ASF_BAD_DATA_HEADER = (0x8004CF03, "The ASF file has a bad data header.") + DRM_E_ASF_INVALID_OPERATION = (0x8004CF04, "The intended operation is invalid given the current processing state of the ASF file.") + DRM_E_ASF_AES_PAYLOAD_FOUND = (0x8004CF05, "ND payload extension system found; the file may be encrypted with AES already.") + DRM_E_ASF_EXTENDED_STREAM_PROPERTIES_OBJ_NOT_FOUND = (0x8004CF06, "Extended stream properties object is not found; the file may be in non-supported outdated format.") + DRM_E_ASF_INVALID_DATA = (0x8004CF20, "The packet is overstuffed with data.") + DRM_E_ASF_TOO_MANY_PAYLOADS = (0x8004CF21, "The number of payloads in the packet is greater than the maximum allowed.") + DRM_E_ASF_BANDWIDTH_OVERRUN = (0x8004CF22, "An object is overflowing the leaky bucket.") + DRM_E_ASF_INVALID_STREAM_NUMBER = (0x8004CF23, "The stream number is invalid; it is either zero, greater than the maximum value allowed, or has no associated data.") + DRM_E_ASF_LATE_SAMPLE = (0x8004CF24, "A sample was encountered with a presentation time outside of the mux's send window.") + DRM_E_ASF_NOT_ACCEPTING = (0x8004CF25, "The sample does not fit in the remaining payload space.") + DRM_E_ASF_UNEXPECTED = (0x8004CF26, "An unexpected error occurred.") + DRM_E_NONCE_STORE_TOKEN_NOT_FOUND = (0x8004D000, "The matching nonce store token is not found.") + DRM_E_NONCE_STORE_OPEN_STORE = (0x8004D001, "Fail to open nonce store.") + DRM_E_NONCE_STORE_CLOSE_STORE = (0x8004D002, "Fail to close nonce store.") + DRM_E_NONCE_STORE_ADD_LICENSE = (0x8004D003, "There is already a license associated with the nonce store token.") + DRM_E_LICGEN_POLICY_NOT_SUPPORTED = (0x8004D100, "The license generation policy combination is not supported.") + DRM_E_POLICYSTATE_NOT_FOUND = (0x8004D200, "The policy state is not found in the secure store.") + DRM_E_POLICYSTATE_CORRUPTED = (0x8004D201, "The policy state is not stored as a valid internal format in the secure store.") + DRM_E_MOVE_DENIED = (0x8004D300, "The requested move operation was denied by the service.") + DRM_E_INVALID_MOVE_RESPONSE = (0x8004D301, "The move response was incorrectly formed.") + DRM_E_MOVE_NONCE_MISMATCH = (0x8004D302, "The nonce in the repsonse did not match the expected value.") + DRM_E_MOVE_TXID_MISMATCH = (0x8004D303, "The transaction id in the repsonse did not match the expected value.") + DRM_E_MOVE_STORE_OPEN_STORE = (0x8004D304, "Failed to open the move store.") + DRM_E_MOVE_STORE_CLOSE_STORE = (0x8004D305, "Failed to close the move store.") + DRM_E_MOVE_STORE_ADD_DATA = (0x8004D306, "Failed to add data into the move store.") + DRM_E_MOVE_STORE_GET_DATA = (0x8004D307, "Failed to retrieve data from the move store.") + DRM_E_MOVE_FORMAT_INVALID = (0x8004D308, "The format of a move page or index is invalid.") + DRM_E_MOVE_SIGNATURE_INVALID = (0x8004D309, "The signature of a move index is invalid.") + DRM_E_COPY_DENIED = (0x8004D30A, "The requested copy operation was denied by the service.") + DRM_E_XB_OBJECT_NOTFOUND = (0x8004D400, "The extensible binary object was not found.") + DRM_E_XB_INVALID_OBJECT = (0x8004D401, "The extensible binary object format was invalid.") + DRM_E_XB_OBJECT_ALREADY_EXISTS = (0x8004D402, "A single instance extensible binary object was encountered more than once.") + DRM_E_XB_REQUIRED_OBJECT_MISSING = (0x8004D403, "A required extensible binary object was not found during building.") + DRM_E_XB_UNKNOWN_ELEMENT_TYPE = (0x8004D404, "An extensible binary object description contained an element of an unknown type.") + DRM_E_XB_INVALID_VERSION = (0x8004D405, "The serialized object version could not be found in the extensible binary object description.") + DRM_E_XB_MAX_UNKNOWN_CONTAINER_DEPTH = (0x8004D406, "The maximum unknown container depth was reached.") + DRM_E_XB_INVALID_ALIGNMENT = (0x8004D407, "The serialized message buffer is not properly aligned according to the XBinary format description.") + DRM_E_XB_OBJECT_OUT_OF_RANGE = (0x8004D408, "An extensible binary object size or count is out of the range specified by the attributes 'MinSize' and 'MaxSize'.") + DRM_E_KEYFILE_INVALID_PLATFORM = (0x8004D500, "The keyfile does not support the current platform.") + DRM_E_KEYFILE_TOO_LARGE = (0x8004D501, "The keyfile is larger than the maximum supported size.") + DRM_E_KEYFILE_PRIVATE_KEY_NOT_FOUND = (0x8004D502, "The private key requested was not found in the keyfile.") + DRM_E_KEYFILE_CERTIFICATE_CHAIN_NOT_FOUND = (0x8004D503, "The certificate chain requested was not found in the keyfile.") + DRM_E_KEYFILE_KEY_NOT_FOUND = (0x8004D504, "The AES Key ID was not found in the keyfile.") + DRM_E_KEYFILE_UNKNOWN_DECRYPTION_METHOD = (0x8004D505, "Unknown keyfile decryption method.") + DRM_E_KEYFILE_INVALID_SIGNATURE = (0x8004D506, "The keyfile signature was not valid.") + DRM_E_KEYFILE_INTERNAL_DECRYPTION_BUFFER_TOO_SMALL = (0x8004D507, "The internal decryption buffer is too small to hold the encrypted key from the keyfile.") + DRM_E_KEYFILE_PLATFORMID_MISMATCH = (0x8004D508, "Platform ID in the certificate does not match expected value.") + DRM_E_KEYFILE_CERTIFICATE_ISSUER_KEY_MISMATCH = (0x8004D509, "Issuer key of the device certificate does not match public key of the model certificate.") + DRM_E_KEYFILE_ROBUSTNESSVERSION_MISMATCH = (0x8004D50A, "Robustness version in the certificate does not match expected value.") + DRM_E_KEYFILE_FILE_NOT_CLOSED = (0x8004D50B, "The KeyFile Close function was not called before trying to unintialize the KeyFile context.") + DRM_E_KEYFILE_NOT_INITED = (0x8004D50C, "The KeyFile Context was not initialized before trying to use it.") + DRM_E_KEYFILE_FORMAT_INVALID = (0x8004D50D, "The format of the KeyFile was invalid.") + DRM_E_KEYFILE_UPDATE_NOT_ALLOWED = (0x8004D50E, "The keyfile of the device is read only, and updates are not permitted.") + DRM_E_EMPTY_LA_URL = (0x8004D50F, "") + DRM_E_PRND_MESSAGE_VERSION_INVALID = (0x8004D700, "The PRND message version is not supported.") + DRM_E_PRND_MESSAGE_WRONG_TYPE = (0x8004D701, "This method does not processs this PRND message type.") + DRM_E_PRND_MESSAGE_INVALID = (0x8004D702, "The PRND message does not conform to the PRND spec and is therefore invalid.") + DRM_E_PRND_SESSION_ID_INVALID = (0x8004D703, "The Transmitter is unable to process a renewal Registration Request Message using a different session. Use the session matching the previous session ID.") + DRM_E_PRND_PROXIMITY_DETECTION_REQUEST_CHANNEL_TYPE_UNSUPPORTED = (0x8004D704, "The PRND Registration Request Message indicated that it only supports Proximity Detection Channel Types that the Transmitter does not support.") + DRM_E_PRND_PROXIMITY_DETECTION_RESPONSE_INVALID = (0x8004D705, "The PRND Proximity Detection Response Message was successfully parsed but the nonce is invalid.") + DRM_E_PRND_PROXIMITY_DETECTION_RESPONSE_TIMEOUT = (0x8004D706, "The PRND Proximity Detection Response Message was successfully processed but did not arrive in time to verify that the Receiver is Near the Transmitter.") + DRM_E_PRND_LICENSE_REQUEST_CID_CALLBACK_REQUIRED = (0x8004D707, "The PRND License Request Message used Content Identifier Type Custom. A Content Identifier Callback to convert the value to a KID is required.") + DRM_E_PRND_LICENSE_RESPONSE_CLMID_INVALID = (0x8004D708, "The PRND License Response Message had an invalid Current License Message ID.") + DRM_E_PRND_CERTIFICATE_NOT_RECEIVER = (0x8004D709, "The PRND Registration Request Message did not include a PlayReady certificate that supports the RECEIVER feature.") + DRM_E_PRND_CANNOT_RENEW_USING_NEW_SESSION = (0x8004D70A, "The Receiver is unable to generate a renewal Registration Request Message using a new session. Use the existing session.") + DRM_E_PRND_INVALID_CUSTOM_DATA_TYPE = (0x8004D70B, "The Custom Data type is invalid. The first four bytes of Custom Data Type ID cannot be 0x4d534654 ( MSFT in ascii ).") + DRM_E_PRND_CLOCK_OUT_OF_SYNC = (0x8004D70C, "The clock on the Receiver is not synchronized with the clock on the Transmitter. Synchronize the clocks.") + DRM_E_PRND_CANNOT_REBIND_PRND_RECEIVED_LICENSE = (0x8004D70D, "The license cannot be rebound to the PRND Receiver because it was itself received from a PRND Transmitter.") + DRM_E_PRND_CANNOT_REGISTER_USING_EXISTING_SESSION = (0x8004D70E, "The Receiver is unable to generate a non-renewal Registration Request Message using an existing session. End the existing session first or use a new session.") + DRM_E_PRND_BUSY_PERFORMING_RENEWAL = (0x8004D70F, "The Receiver is currently unable to process a message of this type because it is in the middle of renewing the session.") + DRM_E_PRND_LICENSE_REQUEST_INVALID_ACTION = (0x8004D710, "Play with no qualifier during license request is all that's supported in v1 of the PRND protocol.") + DRM_E_PRND_TRANSMITTER_UNAUTHORIZED = (0x8004D711, "The Transmitter attempted to authorize with the Receiver but was unsuccessful.") + DRM_E_PRND_TX_SESSION_EXPIRED = (0x8004D712, "The Transmitter session is expired.") + DRM_E_PRND_INCOMPLETE_PROXIMITY_DETECTION = (0x8004D713, "The proximity detection hasn't completed successfully.") + DRM_E_PRND_INVALID_CERT_DIGEST = (0x8004D714, "The given certificate digest doesn't match the one stored in the MTKB") + DRM_E_OEMHAL_NOT_INITIALIZED = (0x8004D780, "The OEM HAL is not initialized.") + DRM_E_OEMHAL_OUT_OF_KEY_REGISTERS = (0x8004D781, "There are no more key registers available in the OEM HAL implementation.") + DRM_E_OEMHAL_KEYS_IN_USE = (0x8004D782, "The OEM HAL is being shutdown whilst keys are still allocated.") + DRM_E_OEMHAL_NO_KEY = (0x8004D783, "The requested preloaded key is not available or the key handle is otherwise invalid.") + DRM_E_OEMHAL_UNSUPPORTED_KEY_TYPE = (0x8004D784, "The specified key type cannot be used for the operation requested.") + DRM_E_OEMHAL_UNSUPPORTED_KEY_WRAPPING_FORMAT = (0x8004D785, "The specified wrapping key type cannot be used to unwrap the specified key.") + DRM_E_OEMHAL_UNSUPPORTED_KEY_LENGTH = (0x8004D786, "The key buffer provided is of the wrong length for the specified key/wrapping key combination.") + DRM_E_OEMHAL_UNSUPPORTED_HASH_TYPE = (0x8004D787, "The specified hash type is not supported.") + DRM_E_OEMHAL_UNSUPPORTED_SIGNATURE_SCHEME = (0x8004D788, "The specified signature scheme is not supported.") + DRM_E_OEMHAL_BUFFER_TOO_LARGE = (0x8004D789, "The output buffer is larger than the input buffer and must be the same size.") + DRM_E_OEMHAL_SAMPLE_ENCRYPTION_MODE_NOT_PERMITTED = (0x8004D78A, "The sample encryption mode is not permitted for this combination of encrypt parameters.") + DRM_E_M2TS_PAT_PID_IS_NOT_ZERO = (0x8004D800, "PID 0 is reserved for PAT and cannot be used for other type of packet.") + DRM_E_M2TS_PTS_NOT_EXIST = (0x8004D801, "The audio/video PES doesn' have the PTS data.") + DRM_E_M2TS_PES_PACKET_LENGTH_NOT_SPECIFIED = (0x8004D802, "The audio PES' packet length is 0 which is not allowed.") + DRM_E_M2TS_OUTPUT_BUFFER_FULL = (0x8004D803, "The output buffer for receiving the encrypted packets is full.") + DRM_E_M2TS_CONTEXT_NOT_INITIALIZED = (0x8004D804, "The encryptor context hasn't been initialized yet.") + DRM_E_M2TS_NEED_KEY_DATA = (0x8004D805, "The key data for encrypting the sample is either hasn't been set or the encryptor needs next key due to key rotation.") + DRM_E_M2TS_DDPLUS_FORMAT_INVALID = (0x8004D806, "Not supported DDPlus format.") + DRM_E_M2TS_NOT_UNIT_START_PACKET = (0x8004D807, "The encryptor expects a unit start packet. The unit start packet should appear before the rest of the packets in the unit.") + DRM_E_M2TS_TOO_MANY_SUBSAMPLES = (0x8004D808, "Too many subsamples over the limit that the ECM allows.") + DRM_E_M2TS_TABLE_ID_INVALID = (0x8004D809, "The PAT or PMT packet contains an invalid table ID.") + DRM_E_M2TS_PACKET_SYNC_BYTE_INVALID = (0x8004D80A, "The TS packet doesn't start with the 0x47 (sync byte).") + DRM_E_M2TS_ADAPTATION_LENGTH_INVALID = (0x8004D80B, "The adaptation field length is invalid.") + DRM_E_M2TS_PAT_HEADER_INVALID = (0x8004D80C, "There is an error in PAT header, unable to parse it.") + DRM_E_M2TS_PMT_HEADER_INVALID = (0x8004D80D, "There is an error in PMT header, unable to parse it.") + DRM_E_M2TS_PES_START_CODE_NOT_FOUND = (0x8004D80E, "Cannot find the PES start code (0x000001).") + DRM_E_M2TS_STREAM_OR_PACKET_TYPE_CHANGED = (0x8004D80F, "The stream type or packet type of an existing PID has changed") + DRM_E_M2TS_INTERNAL_ERROR = (0x8004D810, "An internal error occurred during encryptrion.") + DRM_E_M2TS_ADTS_FORMAT_INVALID = (0x8004D811, "Not supported ADTS format.") + DRM_E_M2TS_MPEGA_FORMAT_INVALID = (0x8004D812, "Not supported MPEGA format.") + DRM_E_M2TS_CA_DESCRIPTOR_LENGTH_INVALID = (0x8004D813, "The CA_descruptor length is greater than ES_info length.") + DRM_E_M2TS_CRC_FIELD_INVALID = (0x8004D814, "The CRC field in the PAT or PMT packet is invalid.") + DRM_E_M2TS_INCOMPLETE_SECTION_HEADER = (0x8004D815, "The section header of a PES is not completed while the next PES packet has started already") + DRM_E_M2TS_INVALID_UNALIGNED_DATA = (0x8004D816, "Not allowed to have the overflow of the unaligned payload to accross more than one PES") + DRM_E_M2TS_GET_ENCRYPTED_DATA_FIRST = (0x8004D817, "Do not pass additional data for encryption when the last encryption result is DRM_S_MORE_DATA") + DRM_E_M2TS_CANNOT_CHANGE_PARAMETER = (0x8004D818, "Not allowed to change the encryption parameter once the encryption started, i.e. after Drm_M2ts_Encrypt is called") + DRM_E_M2TS_UNKNOWN_PACKET = (0x8004D819, "This packet appears before the first PAT and/or PMT and will be dropped.") + DRM_E_M2TS_DROP_PACKET = (0x8004D820, "This packet should be dropped because at least one field in the packet contains an invalid data.") + DRM_E_M2TS_DROP_PES = (0x8004D821, "This PES packet should be dropped because the PES packet is not valid.") + DRM_E_M2TS_INCOMPLETE_PES = (0x8004D822, "This PES packet has one or more missing packets.") + DRM_E_M2TS_WAITED_TOO_LONG = (0x8004D823, "This packet is dropped because its unit is not completed after a long period of time.") + DRM_E_M2TS_SECTION_LENGTH_INVALID = (0x8004D824, "The section length inside the PAT or PMT is less than the minimun PAT or PMT section.") + DRM_E_M2TS_PROGRAM_INFO_LENGTH_INVALID = (0x8004D825, "The sum of the program info length and the other fields in the PMT section don't match with the section length.") + DRM_E_M2TS_PES_HEADER_INVALID = (0x8004D826, "Failed to parse the PES header, the PES maybe too short.") + DRM_E_M2TS_ECM_PAYLOAD_OVER_LIMIT = (0x8004D827, "The size of the ECM payload exceeds the limit of 64k bytes") + DRM_E_M2TS_SET_CA_PID_FAILED = (0x8004D828, "Unable to assign a PID for CA_PID, all PIDs in the range of 0x0010 to 0x1FFE are used.") + DRM_E_LICGEN_CANNOT_PERSIST_LICENSE = (0x8004D901, "A non-persistent license cannot be stored in the license store.") + DRM_E_LICGEN_PERSISTENT_REMOTE_LICENSE = (0x8004D902, "A remote bound license should be non-persistent.") + DRM_E_LICGEN_EXPIRE_AFTER_FIRST_PLAY_REMOTE_LICENSE = (0x8004D903, "A remote bound license should not have expire after first play property.") + DRM_E_LICGEN_ROOT_LICENSE_CANNOT_ENCRYPT = (0x8004D904, "A root license should not be used to encrypt content.") + DRM_E_LICGEN_EMBED_LOCAL_LICENSE = (0x8004D905, "A local bound license cannot be embedded.") + DRM_E_LICGEN_LOCAL_LICENSE_WITH_REMOTE_CERTIFICATE = (0x8004D906, "A local bound license cannot be bound to a remote certificate.") + DRM_E_LICGEN_PLAY_ENABLER_REMOTE_LICENSE = (0x8004D907, "A remote bound license cannot have play enablers other than Passing to Unknown Output or Passing Constrained Resolution to Unknown Output.") + DRM_E_LICGEN_DUPLICATE_PLAY_ENABLER = (0x8004D908, "A license descriptor contains a duplicate play enabler.") + DRM_E_LICGEN_CHILD_SECURITY_LEVEL_TOO_LOW = (0x8004D909, "The security level of the chained license is too low.") + DRM_E_H264_PARSING_FAILED = (0x8004DA00, "The H264 was unable to be parsed.") + DRM_E_H264_SPS_PROFILE = (0x8004DA01, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_IDC = (0x8004DA02, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_SPSID = (0x8004DA03, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_FRAMENUM = (0x8004DA04, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_POCTYPE = (0x8004DA05, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_POCLSB = (0x8004DA06, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_POCCYCLE = (0x8004DA07, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_NUMREFFRAMES = (0x8004DA08, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_CHROMATOP = (0x8004DA09, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_CHROMABOTTOM = (0x8004DA0A, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_NALHRD = (0x8004DA0B, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_VLDHRD = (0x8004DA0C, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_VUIBPPD = (0x8004DA0D, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_VUIBPMD = (0x8004DA0E, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_VUIMMLH = (0x8004DA0F, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_VUIMMLV = (0x8004DA10, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_VUINRF = (0x8004DA11, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_VUIMDFB = (0x8004DA12, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_WIDTH_HEIGHT = (0x8004DA13, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_AREA = (0x8004DA14, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_MINHEIGHT2 = (0x8004DA15, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_MINHEIGHT3 = (0x8004DA16, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_CROPWIDTH = (0x8004DA17, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_CROPHEIGHT = (0x8004DA18, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_MORE_RBSP = (0x8004DA19, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_CHROMA_IDC = (0x8004DA1A, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_BITDEPTHLUMA = (0x8004DA1B, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_BITDEPTHCHROMA = (0x8004DA1C, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_DELTASCALE1 = (0x8004DA1D, "SPS-specific H264 parsing error") + DRM_E_H264_SPS_DELTASCALE2 = (0x8004DA1E, "SPS-specific H264 parsing error") + DRM_E_H264_BITSTREAM_TOOMANY = (0x8004DA30, "Bitstream-specific H264 parsing error") + DRM_E_H264_BITSTREAM_TOOSHORT1 = (0x8004DA31, "Bitstream-specific H264 parsing error") + DRM_E_H264_BITSTREAM_TOOSHORT2 = (0x8004DA32, "Bitstream-specific H264 parsing error") + DRM_E_H264_BITSTREAM_TOOSHORT3 = (0x8004DA33, "Bitstream-specific H264 parsing error") + DRM_E_H264_BITSTREAM_TOOSHORT4 = (0x8004DA34, "Bitstream-specific H264 parsing error") + DRM_E_H264_BITSTREAM_TOOSHORT5 = (0x8004DA35, "Bitstream-specific H264 parsing error") + DRM_E_H264_BITSTREAM_EXGOLOBMTOOLONG1 = (0x8004DA36, "Bitstream-specific H264 parsing error") + DRM_E_H264_BITSTREAM_EXGOLOBMTOOLONG2 = (0x8004DA37, "Bitstream-specific H264 parsing error") + DRM_E_H264_NALU_NO_START_CODE = (0x8004DA40, "Nalu-specific H264 parsing error") + DRM_E_H264_NALU_ALL_ZERO = (0x8004DA41, "Nalu-specific H264 parsing error") + DRM_E_H264_NALU_EMULATION = (0x8004DA42, "Nalu-specific H264 parsing error") + DRM_E_H264_PPS_PPSID = (0x8004DA50, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_SPSID = (0x8004DA51, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_SPS_NOT_FOUND = (0x8004DA52, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_NUM_SLICE_GROUPS = (0x8004DA53, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_SLICE_GROUP_MAX = (0x8004DA54, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_RUN_LENGTH = (0x8004DA55, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_TOP_LEFT = (0x8004DA56, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_SLICE_GROUP_RATE = (0x8004DA57, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_SLICE_GROUP_MAP = (0x8004DA58, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_SLICE_GROUP_ID = (0x8004DA59, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_REF_IDX_L0 = (0x8004DA5A, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_REF_IDX_L1 = (0x8004DA5B, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_WEIGHTED_BIPRED = (0x8004DA5C, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_PIC_INIT_QP = (0x8004DA5D, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_PIC_INIT_QS = (0x8004DA5E, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_PIC_CHROMA_QP = (0x8004DA5F, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_REDUN_PIC_COUNT = (0x8004DA61, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_DELTA_SCALE1 = (0x8004DA62, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_DELTA_SCALE2 = (0x8004DA63, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_SECOND_CHROMA_QP = (0x8004DA64, "PPS-specific H264 parsing error") + DRM_E_H264_PPS_MORE_RBSP = (0x8004DA65, "PPS-specific H264 parsing error") + DRM_E_H264_SH_SLICE_TYPE = (0x8004DA70, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_SLICE_TYPE_UNSUPPORTED = (0x8004DA71, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_PPSID = (0x8004DA72, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_PPS_NOT_FOUND = (0x8004DA73, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_SPS_NOT_FOUND = (0x8004DA74, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_SLICE_TYPE_PROFILE = (0x8004DA75, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_IDR_FRAME_NUM = (0x8004DA76, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_FIRST_MB_IN_SLICE = (0x8004DA77, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_IDR_PIC_ID = (0x8004DA78, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_REDUN_PIC_COUNT = (0x8004DA79, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_NUM_REF_IDX_LX0 = (0x8004DA7A, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_NUM_REF_IDX_LX1 = (0x8004DA7B, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_REF_PIC_LIST_REORDER0 = (0x8004DA7C, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_REF_PIC_LIST_REORDER1 = (0x8004DA7D, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_LUMA_WEIGHT_DENOM = (0x8004DA7E, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_CHROMA_WEIGHT_DENOM = (0x8004DA7F, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_WP_WEIGHT_LUMA0 = (0x8004DA80, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_WP_OFFSET_LUMA0 = (0x8004DA81, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_WP_WEIGHT_CHROMA0 = (0x8004DA82, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_WP_OFFSET_CHROMA0 = (0x8004DA83, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_WP_WEIGHT_LUMA1 = (0x8004DA84, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_WP_OFFSET_LUMA1 = (0x8004DA85, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_WP_WEIGHT_CHROMA1 = (0x8004DA86, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_WP_OFFSET_CHROMA1 = (0x8004DA87, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_NUM_REF_PIC_MARKING = (0x8004DA88, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_MMCO4_DUPLICATE = (0x8004DA89, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_MMCO4_MAX_LONG_TERM_FRAME = (0x8004DA8A, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_MMCO5_DUPLICATE = (0x8004DA8B, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_MMCO5_FOLLOWS_MMC06 = (0x8004DA8C, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_MMCO5_COEXIST_MMCO_1_OR_3 = (0x8004DA8D, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_MMCO6_DUPLICATE = (0x8004DA8E, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_MODEL_NUMBER = (0x8004DA8F, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_SLICE_QP = (0x8004DA90, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_LF_ALPHA_C0_OFFSET = (0x8004DA91, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_LF_BETA_OFFSET = (0x8004DA92, "Slice-Header-specific H264 parsing error") + DRM_E_H264_SH_SLICE_GROUP_CHANGE = (0x8004DA93, "Slice-Header-specific H264 parsing error") + DRM_E_RPROV_INVALID_REQUEST = (0x8004DB00, "Invalid Remote provisioning request received.") + DRM_E_RPROV_VERSION_MISSMATCH = (0x8004DB01, "Invalid Remote provisioning version received.") + DRM_E_RPROV_INVALID_RESPONSE = (0x8004DB02, "Invalid response received.") + DRM_E_RPROV_BOOTSTRAP_FAILURE = (0x8004DB03, "Remote provisioning bootstrap failed.") + DRM_E_FIRMWARE_REVOKED = (0x8004DB04, "TEE Firmware is revoked; firmware update necessary.") + DRM_E_RPROV_SKIP_BOOTSTRAP = (0x8004DB05, "Remote provisioning does not need bootstrap.") + DRM_E_SECURESTOP_STORE_CORRUPT = (0x8004DC00, "The secure stop store is corrupted.") + DRM_E_SECURESTOP_SESSION_LOCKED = (0x8004DC02, "The secure stop session is locked and may not be modified.") + DRM_E_SECURESTOP_SESSION_CORRUPT = (0x8004DC03, "The secure stop session data is corrupted.") + DRM_E_SECURESTOP_SESSION_ACTIVE = (0x8004DC04, "The secure stop session is active and cannot be locked.") + DRM_E_SECURESTOP_SESSION_NOT_FOUND = (0x8004DC05, "The secure stop session could not be found in the data store.") + DRM_E_SECURESTOP_INVALID_RESPONSE = (0x8004DC06, "The secure stop response is invalid.") + DRM_E_SECURESTOP_SESSION_STOPPED = (0x8004DC07, "The secure stop session is stopped and may not be used for decryption.") + DRM_E_SECURESTOP_INVALID_PUBLISHER_ID = (0x8004DC08, "Trying to generate a challenge with a publisher ID that doesn't match the one associated with the session.") + DRM_E_SECURESTOP_PUBLISHER_ID_INCONSISTENT = (0x8004DC09, "Licenses acquired within the same session don't have the same secure stop publisher ID.") + DRM_E_SECURESTOP_INCONSISTENT = (0x8004DC0A, "Some licenses acquired within the same session have secure stop while others don't.") + DRM_E_MUTEX_ACQUIRE_FAILED = (0x8004DD00, "The mutex acquire for critical section failed.") + DRM_E_MUTEX_LEAVE_FAILED = (0x8004DD01, "The mutex leave for critical section failed.") + DRM_E_OEM_DPU_IS_BUSY = (0x8004DD02, "The DPU is still busy handling the previous request.") + DRM_E_OEM_DPU_TIMEOUT_FOR_HDCP_TYPE1_LOCK_REQUEST = (0x8004DD03, "The DPU can not finish the command handling from SEC2 before timeout.") + DRM_E_OEM_HDCP_TYPE1_LOCK_FAILED = (0x8004DD04, "The DPU failed to lock HDCP2.2 type as requested by SEC2.") + DRM_E_OEM_HDCP_TYPE1_LOCK_UNKNOWN = (0x8004DD05, "The HDCP type1 lock RESPONSE value set by DPU is unknown.") + DRM_E_OEM_BAR0_PRIV_WRITE_ERROR = (0x8004DD06, "The register could not be written due to PRI failure.") + DRM_E_OEM_BAR0_PRIV_READ_ERROR = (0x8004DD07, "The register could not be read due to PRI failure.") + DRM_E_OEM_DMA_FAILURE = (0x8004DD08, "The DMA transaction failure.") + DRM_E_OEM_UNSUPPORTED_HS_ACTION = (0x8004DD09, "The requested HS action is not supported.") + DRM_E_OEM_UNSUPPORTED_KDF = (0x8004DD0A, "The KDF is not supported in OEM's encryption/decryption implementation.") + DRM_E_OEM_INVALID_AES_CRYPTO_MODE = (0x8004DD0B, "The requested AES crypto mode is invalid.") + DRM_E_OEM_HS_CHK_INVALID_INPUT = (0x8004DD0C, "One or more of the input arguments to HS mode are invalid.") + DRM_E_OEM_HS_CHK_CHIP_NOT_SUPPORTED = (0x8004DD0D, "The GPU does not support playready.") + DRM_E_OEM_HS_CHK_UCODE_REVOKED = (0x8004DD0E, "Current ucode was revoked.") + DRM_E_OEM_HS_CHK_NOT_IN_LSMODE = (0x8004DD0F, "Relevant falcon (depending upon the context from which this error code is being returned) is not in LS mode.") + DRM_E_OEM_HS_CHK_INVALID_LS_PRIV_LEVEL = (0x8004DD10, "Relevant falcon (depending upon the context from which this error code is being returned) is not at proper LS priv level.") + DRM_E_OEM_HS_CHK_INVALID_REGIONCFG = (0x8004DD11, "The REGIONCFG is not set to correct WPR region.") + DRM_E_OEM_HS_CHK_PRIV_SEC_DISABLED_ON_PROD = (0x8004DD12, "The priv sec is unexpectedly disabled on production board.") + DRM_E_OEM_HS_CHK_SW_FUSING_ALLOWED_ON_PROD = (0x8004DD13, "The SW fusing is unexpectedly allowed on production board.") + DRM_E_OEM_HS_CHK_INTERNAL_SKU_ON_PROD = (0x8004DD14, "The SKU is internal on production board.") + DRM_E_OEM_HS_CHK_DEVID_OVERRIDE_ENABLED_ON_PROD = (0x8004DD15, "The devid override is enabled on production board.") + DRM_E_OEM_HS_CHK_INCONSISTENT_PROD_MODES = (0x8004DD16, "The falcons in GPU are not in consistent debug/production mode.") + DRM_E_OEM_HS_CHK_HUB_ENCRPTION_DISABLED = (0x8004DD17, "The hub encryption is not enabled on FB.") + DRM_E_OEM_HS_PR_ILLEGAL_LASSAHS_STATE_AT_HS_ENTRY = (0x8004DD18, "LASSAHS is not in a correct state before doing HS entry") + DRM_E_OEM_HS_PR_ILLEGAL_LASSAHS_STATE_AT_MPK_DECRYPT = (0x8004DD19, "LASSAHS is not in a correct state before doing MPK decryption") + DRM_E_OEM_HS_PR_ILLEGAL_LASSAHS_STATE_AT_HS_EXIT = (0x8004DD1A, "LASSAHS is not in a correct state before doing HS exit") + DRM_E_OEM_PREENTRY_GDK_OUT_OF_MEMORY = (0x8004DD1B, "Preallocation for GDK failed") + DRM_E_OEM_GDK_IMEM_BLOCK_REVALIDATION_FAILED = (0x8004DD1C, "Failed to revalidate imem blocks invalidated during LASSAHS") + DRM_E_OEM_INVALID_PDI = (0x8004DD1D, "The read PDI value is invalid") + DRM_E_OEM_INVALID_SIZE_OF_CDKBDATA = (0x8004DD1E, "The size of NV_KB_CDKBData is not multiple of 16") + DRM_E_SE_CRYPTO_MUTEX_ACQUIRE_FAILED = (0x8004DD1F, "Failed to acquire the Security Engine mutex for cypto operations") + DRM_E_SE_CRYPTO_MUTEX_RELEASE_FAILED = (0x8004DD20, "Failed to release the Security Engine mutex for cypto operations") + DRM_E_SE_CRYPTO_POINT_NOT_ON_CURVE = (0x8004DD21, "Point is not on the elliptic curve") + DRM_E_OEM_WAIT_FOR_BAR0_IDLE_FAILED = (0x8004DD22, "WFI on a PRI read/write failed") + DRM_E_OEM_ERROR_PRI_UNEXPECTED_FAILURE = (0x8004DD23, "DRM errors.") + DRM_E_OEM_CSB_PRIV_WRITE_ERROR = (0x8004DD24, "The register could not be written due to CSB PRI failure.") + DRM_E_OEM_CSB_PRIV_READ_ERROR = (0x8004DD25, "The register could not be read due to CSB PRI failure.") + DRM_E_OEM_HS_MUTEX_ACQUIRE_FAILED = (0x8004DD26, "The mutex acquire for critical section at HS mode failed.") + DRM_E_OEM_HS_MUTEX_RELEASE_FAILED = (0x8004DD27, "The mutex leave for critical section at HS mode failed.") + DRM_E_MUTEX_INVALIDARG = (0x8004DDC1, "The Mutex ID is invalid.") + DRM_E_MUTEX_ACQUIRE_GETTIME_FAILED = (0x8004DDC2, "Failed to get current time during mutex acquisition.") + DRM_E_MUTEX_ACQUIRE_BAD_ID_VALUE = (0x8004DDC3, "The token ID obtained during mutex acquisition is invalid.") + DRM_E_MUTEX_ACQUIRE_BAD_SHAREDATA = (0x8004DDC4, "Frame Buffer address of SEC-NVDEC shared structure is invalid.") + DRM_E_MUTEX_ACQUIRE_TIMEOUT = (0x8004DDC5, "Timeout occured during mutex acquisition.") + DRM_E_SECUREBUS_TIMEOUT = (0x8004DDC6, "Timeout occured on secure bus while waiting for DOC to become empty.") + DRM_E_SECUREBUS_READ_FAILED = (0x8004DDC7, "Read request on secure bus failed.") + DRM_E_MUTEX_OWNERSHIP_MATCH_FAILED = (0x8004DDC8, "Token ID reserved for the mutex in hardware doesn't match with specified token ID.") + DRM_E_INVALID_PDI = (0x8004DDC9, "The read PDI value is invalid.") + DRM_E_SECURETIME_INVALID_REQUEST_DATA = (0x8004DE00, "The secure time client request data is invalid.") + DRM_E_SECURETIME_CLOCK_NOT_SET = (0x8004DE01, "The secure time clock has not been set.") + DRM_E_SECURETIME_RESPONSE_TIMEOUT = (0x8004DE02, "The secure time server response timed out.") + DRM_E_SECURETIME_SERVER_SECURITY_LEVEL_TOO_LOW = (0x8004DE03, "The secure time server's security level is too low for the client.") + DRM_E_LICENSESERVERTIME_MUST_REACQUIRE_LICENSE = (0x8004DE04, "This license was acquired before the LicenseServerTime feature was enabled. It must be reacquired.") + DRM_E_LSRD_DETECTED = (0x8004DF00, "HDS file rollback is detected.") + DRM_E_LSRD_INVALID_ACL = (0x8004DF01, "The ACL of the HDS Registry Subkey is invalid.") + DRM_E_LSRD_DETECTION_IN_PROGRESS = (0x8004DF02, "The client is currently processing LSRD check operation. Concurrent operations are not allowed.") + DRM_E_LSRD_ACL_NOT_PRESENT = (0x8004DF03, "The security descriptor does not contain an ACL.") + DRM_E_LSRD_INVALID_COMMAND = (0x8004DF04, "The PlayReady Process received an invalid command.") + DRM_E_LSRD_SEQUENCE_NUMBER_IS_AT_MAX_LIMIT = (0x8004DF05, "The LSRD sequence number has reached its maximum limit.") + DRM_E_SECUREDELETE_INVALID_RESPONSE = (0x8004DFA0, "The secure delete response is invalid.") + DRM_E_PROVENANCE_VALIDATION_FAILED = (0x8004DFB0, "The provenance validation failed. The media file has been tampered with.") + DRM_E_INVALID_PROVENANCE_MANIFEST = (0x8004DFB1, "The provenance manifest is invalid.") + DRM_E_INVALID_PROVENANCE_CERTIFICATE_CHAIN = (0x8004DFB2, "The provenance certificate chain stored in the manifest is invalid or a valid certificate chain could not be established.") + DRM_E_PROVENANCE_UNTRUSTED_ROOT_CERTIFICATE = (0x8004DFB3, "The provenance certificate chain could not be validated because no root certificates were provided in the trusted list.") + DRM_E_MP4_EXCEEDED_NUM_CHUNKS = (0x8004DFB4, "A query was made to the MP4 parser to get chunk information for a chunk that does not exist.") + DRM_E_MP4_NULL_FILE_STREAM = (0x8004DFB5, "A file stream pointer was unexpectedly null.") + DRM_E_MP4_INVALID_MP4_FILE = (0x8004DFB6, "The MP4 file is malformed.") + DRM_E_MP4_PARSING_ABORTED = (0x8004DFB7, "MP4 parsing was aborted by the caller.") + DRM_E_MP4_EXCEEDED_BOX_SIZE = (0x8004DFB8, "The MP4 file has a box that references data beyond the end of the box.") + DRM_E_MP4_INVALID_BOX_SIZE = (0x8004DFB9, "The MP4 file has an invalid box size.") + DRM_E_MP4_INVALID_PARSING_STATE = (0x8004DFBA, "MP4 parser functions were invoked in an invalid sequence.") + DRM_E_MP4_INVALID_BOX_ATTRIBUTE = (0x8004DFBB, "An MP4 box has a malformed attribute.") + DRM_E_MP4_INVALID_BOX_VERSION = (0x8004DFBC, "An MP4 box had an unrecognized version.") + DRM_E_MP4_INVALID_STTS_CONTAINS_ENTRIES = (0x8004DFBD, "The 'stts' box should not contain entries in purely fragmented Mp4 files.") + DRM_E_C2PA_FTYP_NOT_SET = (0x8004DFBE, "The 'ftyp' box lacks the 'c2pa' compatible_brands attribute.") + DRM_E_MP4_BOX_LARGER_THAN_4GB = (0x8004DFBF, "The MP4 file has a box that is larger than 4 GB in size which is not supported by this parsre.") + DRM_E_MP4_C2PA_BOX_ALREADY_PRESENT = (0x8004DFC0, "The MP4 file already contains an unexpected C2PA Box.") + DRM_E_MP4_CIB_BOX_ALREADY_PRESENT = (0x8004DFC1, "The MP4 file already contains an unexpected Content Integrity Box.") + DRM_E_C2PA_MANIFEST_BOX_NOT_PRESENT = (0x8004DFC2, "The MP4 file lacks the expected c2pa Manifest Box.") + DRM_E_C2PA_MERKLE_BOX_NOT_PRESENT = (0x8004DFC3, "The MP4 file lacks the expected c2pa Merkle Box.") + DRM_E_MP4_INVALID_C2PA_MANIFEST_BOX = (0x8004DFC4, "The MP4 file contains an invalid c2pa box with type Manifest.") + DRM_E_MP4_INVALID_C2PA_MERKLE_BOX = (0x8004DFC5, "The MP4 file contains an invalid c2pa box with type Merkle.") + DRM_E_MP4_INVALID_PARSING_TYPE = (0x8004DFC6, "The MP4 parser was initialized with an invalid type.") + DRM_E_MP4_FILE_LACKS_MOOV_BOX = (0x8004DFC7, "The MP4 file does not have a 'moov' box.") + DRM_E_MP4_EXCEEDED_NUM_TRACK_IDS = (0x8004DFC8, "A query was made to the MP4 parser to get track information for a track that does not exist.") + DRM_E_MP4_INVALID_EXCLUSION_RULE = (0x8004DFC9, "An Exclusion Rule was passed into the MP4 Parser that was incorrectly formatted.") + DRM_E_WIN32_FILE_NOT_FOUND = (0x80070002, "The system cannot find the file specified.") + DRM_E_HANDLE = (0x80070006, "Invalid handle.") + DRM_E_WIN32_NO_MORE_FILES = (0x80070012, "There are no more files.") + DRM_E_INVALIDARG = (0x80070057, "The parameter is incorrect.") + DRM_E_BUFFERTOOSMALL = (0x8007007A, "The data area passed to a function is too small.") + DRM_E_NOMORE = (0x80070103, "No more data is available.") + DRM_E_ARITHMETIC_OVERFLOW = (0x80070216, "Arithmetic result exceeded maximum value.") + DRM_E_NOT_FOUND = (0x80070490, "Element not found.") + DRM_E_INVALID_COMMAND_LINE = (0x80070667, "Invalid command line argument.") + DRM_E_FAILED_TO_STORE_LICENSE = (0xC00D2712, "License storage is not working.") + DRM_E_PARAMETERS_MISMATCHED = (0xC00D272F, "A problem has occurred in the Digital Rights Management component.") + DRM_E_NOT_ALL_STORED = (0xC00D275F, "Some of the licenses could not be stored.") + + @property + def code(self): + """Return the numeric code of the error.""" + return hex(self.value[0]) + + @property + def message(self): + """Return the descriptive message of the error.""" + return self.value[1] + + @staticmethod + def from_code(code: str): + """Get the error message for a given error code.""" + for error in DRMResult: + if error.value[0] == int(code, 16): + return error + raise ValueError("Invalid DRMResult") diff --git a/pyplayready/exceptions.py b/pyplayready/exceptions.py new file mode 100644 index 0000000..a388cb2 --- /dev/null +++ b/pyplayready/exceptions.py @@ -0,0 +1,42 @@ +class PyPlayreadyException(Exception): + """Exceptions used by pyplayready.""" + + +class TooManySessions(PyPlayreadyException): + """Too many Sessions are open.""" + + +class InvalidSession(PyPlayreadyException): + """No Session is open with the specified identifier.""" + + +class InvalidPssh(PyPlayreadyException): + """The Playready PSSH is invalid or empty.""" + + +class InvalidInitData(PyPlayreadyException): + """The Playready Cenc Header Data is invalid or empty.""" + + +class DeviceMismatch(PyPlayreadyException): + """The Remote CDMs Device information and the APIs Device information did not match.""" + + +class InvalidLicense(PyPlayreadyException): + """Unable to parse XMR License.""" + + +class InvalidCertificate(PyPlayreadyException): + """The BCert is not correctly formatted.""" + + +class InvalidCertificateChain(PyPlayreadyException): + """The BCertChain is not correctly formatted.""" + + +class OutdatedDevice(PyPlayreadyException): + """The PlayReady Device is outdated and does not support a specific operation.""" + + +class ServerException(PyPlayreadyException): + """Recasted on the client if found in license response.""" \ No newline at end of file diff --git a/pyplayready/license/key.py b/pyplayready/license/key.py new file mode 100644 index 0000000..54ecca0 --- /dev/null +++ b/pyplayready/license/key.py @@ -0,0 +1,68 @@ +import base64 +from enum import Enum +from uuid import UUID +from typing import Union + + +class Key: + class KeyType(Enum): + INVALID = 0x0000 + AES_128_CTR = 0x0001 + RC4_CIPHER = 0x0002 + AES_128_ECB = 0x0003 + COCKTAIL = 0x0004 + AES_128_CBC = 0x0005 + KEYEXCHANGE = 0x0006 + UNKNOWN = 0xffff + + @classmethod + def _missing_(cls, value): + return cls.UNKNOWN + + class CipherType(Enum): + INVALID = 0x0000 + RSA_1024 = 0x0001 + CHAINED_LICENSE = 0x0002 + ECC_256 = 0x0003 + ECC_256_WITH_KZ = 0x0004 + TEE_TRANSIENT = 0x0005 + ECC_256_VIA_SYMMETRIC = 0x0006 + UNKNOWN = 0xffff + + @classmethod + def _missing_(cls, value): + return cls.UNKNOWN + + def __init__( + self, + key_id: UUID, + key_type: int, + cipher_type: int, + key_length: int, + key: bytes + ): + self.key_id = key_id + self.key_type = self.KeyType(key_type) + self.cipher_type = self.CipherType(cipher_type) + self.key_length = key_length + self.key = key + + @staticmethod + def kid_to_uuid(kid: Union[str, bytes]) -> UUID: + """ + Convert a Key ID from a string or bytes to a UUID object. + At first, this may seem very simple, but some types of Key IDs + may not be 16 bytes and some may be decimal vs. hex. + """ + if isinstance(kid, str): + kid = base64.b64decode(kid) + if not kid: + kid = b"\x00" * 16 + + if kid.decode(errors="replace").isdigit(): + return UUID(int=int(kid.decode())) + + if len(kid) < 16: + kid += b"\x00" * (16 - len(kid)) + + return UUID(bytes=kid) diff --git a/pyplayready/license/xml_key.py b/pyplayready/license/xml_key.py new file mode 100644 index 0000000..cc501fd --- /dev/null +++ b/pyplayready/license/xml_key.py @@ -0,0 +1,22 @@ +from ecpy.curves import Point, Curve + +from pyplayready.crypto.ecc_key import ECCKey +from pyplayready.crypto.elgamal import ElGamal + + +class XmlKey: + """Represents a PlayReady XMLKey""" + + def __init__(self): + self.curve = Curve.get_curve("secp256r1") + + self._shared_point = ECCKey.generate() + self.shared_key_x = self._shared_point.key.pointQ.x + self.shared_key_y = self._shared_point.key.pointQ.y + + self._shared_key_x_bytes = ElGamal.to_bytes(int(self.shared_key_x)) + self.aes_iv = self._shared_key_x_bytes[:16] + self.aes_key = self._shared_key_x_bytes[16:] + + def get_point(self) -> Point: + return Point(self.shared_key_x, self.shared_key_y, self.curve) diff --git a/pyplayready/license/xmrlicense.py b/pyplayready/license/xmrlicense.py new file mode 100644 index 0000000..3ce6492 --- /dev/null +++ b/pyplayready/license/xmrlicense.py @@ -0,0 +1,348 @@ +from __future__ import annotations + +import base64 +from enum import IntEnum +from typing import Union + +from Crypto.Cipher import AES +from Crypto.Hash import CMAC +from construct import Const, GreedyRange, Struct, Int32ub, Bytes, Int16ub, this, Switch, LazyBound, Array, Container + + +class XMRObjectTypes(IntEnum): + INVALID = 0x0000 + OUTER_CONTAINER = 0x0001 + GLOBAL_POLICY_CONTAINER = 0x0002 + MINIMUM_ENVIRONMENT_OBJECT = 0x0003 + PLAYBACK_POLICY_CONTAINER = 0x0004 + OUTPUT_PROTECTION_OBJECT = 0x0005 + UPLINK_KID_OBJECT = 0x0006 + EXPLICIT_ANALOG_VIDEO_OUTPUT_PROTECTION_CONTAINER = 0x0007 + ANALOG_VIDEO_OUTPUT_CONFIGURATION_OBJECT = 0x0008 + KEY_MATERIAL_CONTAINER = 0x0009 + CONTENT_KEY_OBJECT = 0x000A + SIGNATURE_OBJECT = 0x000B + SERIAL_NUMBER_OBJECT = 0x000C + SETTINGS_OBJECT = 0x000D + COPY_POLICY_CONTAINER = 0x000E + ALLOW_PLAYLISTBURN_POLICY_CONTAINER = 0x000F + INCLUSION_LIST_OBJECT = 0x0010 + PRIORITY_OBJECT = 0x0011 + EXPIRATION_OBJECT = 0x0012 + ISSUEDATE_OBJECT = 0x0013 + EXPIRATION_AFTER_FIRSTUSE_OBJECT = 0x0014 + EXPIRATION_AFTER_FIRSTSTORE_OBJECT = 0x0015 + METERING_OBJECT = 0x0016 + PLAYCOUNT_OBJECT = 0x0017 + GRACE_PERIOD_OBJECT = 0x001A + COPYCOUNT_OBJECT = 0x001B + COPY_PROTECTION_OBJECT = 0x001C + PLAYLISTBURN_COUNT_OBJECT = 0x001F + REVOCATION_INFORMATION_VERSION_OBJECT = 0x0020 + RSA_DEVICE_KEY_OBJECT = 0x0021 + SOURCEID_OBJECT = 0x0022 + REVOCATION_CONTAINER = 0x0025 + RSA_LICENSE_GRANTER_KEY_OBJECT = 0x0026 + USERID_OBJECT = 0x0027 + RESTRICTED_SOURCEID_OBJECT = 0x0028 + DOMAIN_ID_OBJECT = 0x0029 + ECC_DEVICE_KEY_OBJECT = 0x002A + GENERATION_NUMBER_OBJECT = 0x002B + POLICY_METADATA_OBJECT = 0x002C + OPTIMIZED_CONTENT_KEY_OBJECT = 0x002D + EXPLICIT_DIGITAL_AUDIO_OUTPUT_PROTECTION_CONTAINER = 0x002E + RINGTONE_POLICY_CONTAINER = 0x002F + EXPIRATION_AFTER_FIRSTPLAY_OBJECT = 0x0030 + DIGITAL_AUDIO_OUTPUT_CONFIGURATION_OBJECT = 0x0031 + REVOCATION_INFORMATION_VERSION_2_OBJECT = 0x0032 + EMBEDDING_BEHAVIOR_OBJECT = 0x0033 + SECURITY_LEVEL = 0x0034 + COPY_TO_PC_CONTAINER = 0x0035 + PLAY_ENABLER_CONTAINER = 0x0036 + MOVE_ENABLER_OBJECT = 0x0037 + COPY_ENABLER_CONTAINER = 0x0038 + PLAY_ENABLER_OBJECT = 0x0039 + COPY_ENABLER_OBJECT = 0x003A + UPLINK_KID_2_OBJECT = 0x003B + COPY_POLICY_2_CONTAINER = 0x003C + COPYCOUNT_2_OBJECT = 0x003D + RINGTONE_ENABLER_OBJECT = 0x003E + EXECUTE_POLICY_CONTAINER = 0x003F + EXECUTE_POLICY_OBJECT = 0x0040 + READ_POLICY_CONTAINER = 0x0041 + EXTENSIBLE_POLICY_RESERVED_42 = 0x0042 + EXTENSIBLE_POLICY_RESERVED_43 = 0x0043 + EXTENSIBLE_POLICY_RESERVED_44 = 0x0044 + EXTENSIBLE_POLICY_RESERVED_45 = 0x0045 + EXTENSIBLE_POLICY_RESERVED_46 = 0x0046 + EXTENSIBLE_POLICY_RESERVED_47 = 0x0047 + EXTENSIBLE_POLICY_RESERVED_48 = 0x0048 + EXTENSIBLE_POLICY_RESERVED_49 = 0x0049 + EXTENSIBLE_POLICY_RESERVED_4A = 0x004A + EXTENSIBLE_POLICY_RESERVED_4B = 0x004B + EXTENSIBLE_POLICY_RESERVED_4C = 0x004C + EXTENSIBLE_POLICY_RESERVED_4D = 0x004D + EXTENSIBLE_POLICY_RESERVED_4E = 0x004E + EXTENSIBLE_POLICY_RESERVED_4F = 0x004F + REMOVAL_DATE_OBJECT = 0x0050 + AUX_KEY_OBJECT = 0x0051 + UPLINKX_OBJECT = 0x0052 + INVALID_RESERVED_53 = 0x0053 + APPLICATION_ID_LIST = 0x0054 + REAL_TIME_EXPIRATION = 0x0055 + ND_TX_AUTH_CONTAINER = 0x0056 + ND_TX_AUTH_OBJECT = 0x0057 + EXPLICIT_DIGITAL_VIDEO_PROTECTION = 0x0058 + DIGITAL_VIDEO_OPL = 0x0059 + SECURESTOP = 0x005A + SECURESTOP2 = 0x005C + OPTIMIZED_CONTENT_KEY2 = 0x005D + COPY_UNKNOWN_OBJECT = 0xFFFD + PLAYBACK_UNKNOWN_OBJECT = 0xFFFD + GLOBAL_POLICY_UNKNOWN_OBJECT = 0xFFFD + COPY_UNKNOWN_CONTAINER = 0xFFFE + PLAYBACK_UNKNOWN_CONTAINER = 0xFFFE + UNKNOWN_CONTAINERS = 0xFFFE + + +class _XMRLicenseStructs: + PlayEnablerType = Struct( + "player_enabler_type" / Bytes(16) + ) + + DomainRestrictionObject = Struct( + "account_id" / Bytes(16), + "revision" / Int32ub + ) + + IssueDateObject = Struct( + "issue_date" / Int32ub + ) + + RevInfoVersionObject = Struct( + "sequence" / Int32ub + ) + + SecurityLevelObject = Struct( + "minimum_security_level" / Int16ub + ) + + EmbeddedLicenseSettingsObject = Struct( + "indicator" / Int16ub + ) + + ECCKeyObject = Struct( + "curve_type" / Int16ub, + "key_length" / Int16ub, + "key" / Bytes(this.key_length) + ) + + SignatureObject = Struct( + "signature_type" / Int16ub, + "signature_data_length" / Int16ub, + "signature_data" / Bytes(this.signature_data_length) + ) + + ContentKeyObject = Struct( + "key_id" / Bytes(16), + "key_type" / Int16ub, + "cipher_type" / Int16ub, + "key_length" / Int16ub, + "encrypted_key" / Bytes(this.key_length) + ) + + RightsSettingsObject = Struct( + "rights" / Int16ub + ) + + OutputProtectionLevelRestrictionObject = Struct( + "minimum_compressed_digital_video_opl" / Int16ub, + "minimum_uncompressed_digital_video_opl" / Int16ub, + "minimum_analog_video_opl" / Int16ub, + "minimum_digital_compressed_audio_opl" / Int16ub, + "minimum_digital_uncompressed_audio_opl" / Int16ub, + ) + + ExpirationRestrictionObject = Struct( + "begin_date" / Int32ub, + "end_date" / Int32ub + ) + + RemovalDateObject = Struct( + "removal_date" / Int32ub + ) + + UplinkKIDObject = Struct( + "uplink_kid" / Bytes(16), + "chained_checksum_type" / Int16ub, + "chained_checksum_length" / Int16ub, + "chained_checksum" / Bytes(this.chained_checksum_length) + ) + + AnalogVideoOutputConfigurationRestriction = Struct( + "video_output_protection_id" / Bytes(16), + "binary_configuration_data" / Bytes(this._.length - 24) + ) + + DigitalVideoOutputRestrictionObject = Struct( + "video_output_protection_id" / Bytes(16), + "binary_configuration_data" / Bytes(this._.length - 24) + ) + + DigitalAudioOutputRestrictionObject = Struct( + "audio_output_protection_id" / Bytes(16), + "binary_configuration_data" / Bytes(this._.length - 24) + ) + + PolicyMetadataObject = Struct( + "metadata_type" / Bytes(16), + "policy_data" / Bytes(this._.length - 24) + ) + + SecureStopRestrictionObject = Struct( + "metering_id" / Bytes(16) + ) + + MeteringRestrictionObject = Struct( + "metering_id" / Bytes(16) + ) + + ExpirationAfterFirstPlayRestrictionObject = Struct( + "seconds" / Int32ub + ) + + GracePeriodObject = Struct( + "grace_period" / Int32ub + ) + + SourceIdObject = Struct( + "source_id" / Int32ub + ) + + AuxiliaryKey = Struct( + "location" / Int32ub, + "key" / Bytes(16) + ) + + AuxiliaryKeysObject = Struct( + "count" / Int16ub, + "auxiliary_keys" / Array(this.count, AuxiliaryKey) + ) + + UplinkKeyObject3 = Struct( + "uplink_key_id" / Bytes(16), + "chained_length" / Int16ub, + "checksum" / Bytes(this.chained_length), + "count" / Int16ub, + "entries" / Array(this.count, Int32ub) + ) + + CopyEnablerObject = Struct( + "copy_enabler_type" / Bytes(16) + ) + + CopyCountRestrictionObject = Struct( + "count" / Int32ub + ) + + MoveObject = Struct( + "minimum_move_protection_level" / Int32ub + ) + + XmrObject = Struct( + "flags" / Int16ub, + "type" / Int16ub, + "length" / Int32ub, + "data" / Switch( + lambda ctx: ctx.type, + { + XMRObjectTypes.OUTPUT_PROTECTION_OBJECT: OutputProtectionLevelRestrictionObject, + XMRObjectTypes.ANALOG_VIDEO_OUTPUT_CONFIGURATION_OBJECT: AnalogVideoOutputConfigurationRestriction, + XMRObjectTypes.CONTENT_KEY_OBJECT: ContentKeyObject, + XMRObjectTypes.SIGNATURE_OBJECT: SignatureObject, + XMRObjectTypes.SETTINGS_OBJECT: RightsSettingsObject, + XMRObjectTypes.EXPIRATION_OBJECT: ExpirationRestrictionObject, + XMRObjectTypes.ISSUEDATE_OBJECT: IssueDateObject, + XMRObjectTypes.METERING_OBJECT: MeteringRestrictionObject, + XMRObjectTypes.GRACE_PERIOD_OBJECT: GracePeriodObject, + XMRObjectTypes.SOURCEID_OBJECT: SourceIdObject, + XMRObjectTypes.ECC_DEVICE_KEY_OBJECT: ECCKeyObject, + XMRObjectTypes.DOMAIN_ID_OBJECT: DomainRestrictionObject, + XMRObjectTypes.POLICY_METADATA_OBJECT: PolicyMetadataObject, + XMRObjectTypes.EXPIRATION_AFTER_FIRSTPLAY_OBJECT: ExpirationAfterFirstPlayRestrictionObject, + XMRObjectTypes.DIGITAL_AUDIO_OUTPUT_CONFIGURATION_OBJECT: DigitalAudioOutputRestrictionObject, + XMRObjectTypes.REVOCATION_INFORMATION_VERSION_2_OBJECT: RevInfoVersionObject, + XMRObjectTypes.EMBEDDING_BEHAVIOR_OBJECT: EmbeddedLicenseSettingsObject, + XMRObjectTypes.SECURITY_LEVEL: SecurityLevelObject, + XMRObjectTypes.MOVE_ENABLER_OBJECT: MoveObject, + XMRObjectTypes.PLAY_ENABLER_OBJECT: PlayEnablerType, + XMRObjectTypes.COPY_ENABLER_OBJECT: CopyEnablerObject, + XMRObjectTypes.UPLINK_KID_2_OBJECT: UplinkKIDObject, + XMRObjectTypes.COPYCOUNT_2_OBJECT: CopyCountRestrictionObject, + XMRObjectTypes.REMOVAL_DATE_OBJECT: RemovalDateObject, + XMRObjectTypes.AUX_KEY_OBJECT: AuxiliaryKeysObject, + XMRObjectTypes.UPLINKX_OBJECT: UplinkKeyObject3, + XMRObjectTypes.DIGITAL_VIDEO_OPL: DigitalVideoOutputRestrictionObject, + XMRObjectTypes.SECURESTOP: SecureStopRestrictionObject, + }, + default=LazyBound(lambda ctx: _XMRLicenseStructs.XmrObject) + ) + ) + + XmrLicense = Struct( + "signature" / Const(b"XMR\x00"), + "xmr_version" / Int32ub, + "rights_id" / Bytes(16), + "containers" / GreedyRange(XmrObject) + ) + + +class XMRLicense(_XMRLicenseStructs): + """Represents an XMRLicense""" + + def __init__( + self, + parsed_license: Container, + license_obj: _XMRLicenseStructs.XmrLicense = _XMRLicenseStructs.XmrLicense + ): + self.parsed = parsed_license + self._license_obj = license_obj + + @classmethod + def loads(cls, data: Union[str, bytes]) -> XMRLicense: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + licence = _XMRLicenseStructs.XmrLicense + return cls( + parsed_license=licence.parse(data), + license_obj=licence + ) + + def dumps(self) -> bytes: + return self._license_obj.build(self.parsed) + + def _locate(self, container: Container): + if container.flags == 2 or container.flags == 3: + return self._locate(container.data) + else: + return container + + def get_object(self, type_: int): + for obj in self.parsed.containers: + container = self._locate(obj) + if container.type == type_: + yield container.data + + def get_content_keys(self): + yield from self.get_object(XMRObjectTypes.CONTENT_KEY_OBJECT) + + def check_signature(self, integrity_key: bytes) -> bool: + cmac = CMAC.new(integrity_key, ciphermod=AES) + + signature_data = next(self.get_object(XMRObjectTypes.SIGNATURE_OBJECT)) + cmac.update(self.dumps()[:-(signature_data.signature_data_length + 12)]) + + return signature_data.signature_data == cmac.digest() diff --git a/pyplayready/main.py b/pyplayready/main.py new file mode 100644 index 0000000..53ce0c3 --- /dev/null +++ b/pyplayready/main.py @@ -0,0 +1,332 @@ +import logging +from datetime import datetime +from pathlib import Path +from typing import Optional + +import click +import requests +from Crypto.Random import get_random_bytes + +from pyplayready import __version__, InvalidCertificateChain, InvalidLicense +from pyplayready.system.bcert import CertificateChain, Certificate +from pyplayready.cdm import Cdm +from pyplayready.device import Device +from pyplayready.crypto.ecc_key import ECCKey +from pyplayready.exceptions import OutdatedDevice +from pyplayready.system.pssh import PSSH + + +@click.group(invoke_without_command=True) +@click.option("-v", "--version", is_flag=True, default=False, help="Print version information.") +@click.option("-d", "--debug", is_flag=True, default=False, help="Enable DEBUG level logs.") +def main(version: bool, debug: bool) -> None: + """Python PlayReady CDM implementation""" + logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) + log = logging.getLogger() + + current_year = datetime.now().year + copyright_years = f"2024-{current_year}" + + log.info("pyplayready version %s Copyright (c) %s DevLARLEY, Erevoc, DevataDev", __version__, copyright_years) + log.info("https://github.com/ready-dl/pyplayready") + log.info("Run 'pyplayready --help' for help") + if version: + return + + +@main.command(name="license") +@click.argument("device_path", type=Path) +@click.argument("pssh", type=PSSH) +@click.argument("server", type=str) +def license_(device_path: Path, pssh: PSSH, server: str) -> None: + """ + Make a License Request to a server using a given PSSH + Will return a list of all keys within the returned license + + Only works for standard license servers that don't use any license wrapping + """ + log = logging.getLogger("license") + + device = Device.load(device_path) + log.info(f"Loaded Device: {device.get_name()}") + + cdm = Cdm.from_device(device) + log.info("Loaded CDM") + + session_id = cdm.open() + log.info("Opened Session") + + challenge = cdm.get_license_challenge(session_id, pssh.wrm_headers[0]) + log.info("Created License Request (Challenge)") + log.debug(challenge) + + license_response = requests.post( + url=server, + headers={ + 'Content-Type': 'text/xml; charset=UTF-8', + }, + data=challenge + ) + + licence = license_response.text + log.debug(licence) + + try: + cdm.parse_license(session_id, licence) + except InvalidLicense as e: + log.error(e) + return + + log.info("License Parsed Successfully") + + for key in cdm.get_keys(session_id): + log.info(f"{key.key_id.hex}:{key.key.hex()}") + + cdm.close(session_id) + log.info("Clossed Session") + + +@main.command() +@click.argument("device", type=Path) +@click.option("-c", "--ckt", type=click.Choice(["aesctr", "aescbc"], case_sensitive=False), default="aesctr", help="Content Key Encryption Type") +@click.option("-sl", "--security_level", type=click.Choice(["150", "2000", "3000"]), default="2000", help="Minimum Security Level") +@click.pass_context +def test(ctx: click.Context, device: Path, ckt: str, security_level: str) -> None: + """ + Test the CDM code by getting Content Keys for the Tears Of Steel demo on the Playready Test Server. + https://testweb.playready.microsoft.com/Content/Content2X + + DASH Manifest URL: https://test.playready.microsoft.com/media/profficialsite/tearsofsteel_4k.ism/manifest.mpd + + MSS Manifest URL: https://test.playready.microsoft.com/media/profficialsite/tearsofsteel_4k.ism.smoothstreaming/manifest + + The device argument is a Path to a Playready Device (.prd) file which contains the device's group key and + group certificate. + """ + pssh = PSSH( + "AAADfHBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAA1xcAwAAAQABAFIDPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AH" + "QAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABh" + "AHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUg" + "BPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQA" + "UgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgA0AFIAcABsAGIAKwBUAGIATgBFAFMAOAB0AE" + "cAawBOAEYAVwBUAEUASABBAD0APQA8AC8ASwBJAEQAPgA8AEMASABFAEMASwBTAFUATQA+AEsATABqADMAUQB6AFEAUAAvAE4AQQA9ADwALwBD" + "AEgARQBDAEsAUwBVAE0APgA8AEwAQQBfAFUAUgBMAD4AaAB0AHQAcABzADoALwAvAHAAcgBvAGYAZgBpAGMAaQBhAGwAcwBpAHQAZQAuAGsAZQ" + "B5AGQAZQBsAGkAdgBlAHIAeQAuAG0AZQBkAGkAYQBzAGUAcgB2AGkAYwBlAHMALgB3AGkAbgBkAG8AdwBzAC4AbgBlAHQALwBQAGwAYQB5AFIA" + "ZQBhAGQAeQAvADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwASQBJAFMAXwBEAFIATQBfAF" + "YARQBSAFMASQBPAE4APgA4AC4AMQAuADIAMwAwADQALgAzADEAPAAvAEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4APAAvAEMAVQBT" + "AFQATwBNAEEAVABUAFIASQBCAFUAVABFAFMAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==" + ) + + license_server = f"https://test.playready.microsoft.com/service/rightsmanager.asmx?cfg=(persist:false,sl:{security_level},ckt:{ckt})" + + ctx.invoke( + license_, + device_path=device, + pssh=pssh, + server=license_server + ) + + +@main.command() +@click.option("-k", "--group_key", type=Path, required=True, help="Device ECC private group key") +@click.option("-e", "--encryption_key", type=Path, required=False, help="Optional Device ECC private encryption key") +@click.option("-s", "--signing_key", type=Path, required=False, help="Optional Device ECC private signing key") +@click.option("-c", "--group_certificate", type=Path, required=True, help="Device group certificate chain") +@click.option("-o", "--output", type=Path, default=None, help="Output Path or Directory") +@click.pass_context +def create_device( + ctx: click.Context, + group_key: Path, + encryption_key: Optional[Path], + signing_key: Optional[Path], + group_certificate: Path, + output: Optional[Path] = None +) -> None: + """Create a Playready Device (.prd) file from an ECC private group key and group certificate chain""" + if not group_key.is_file(): + raise click.UsageError("group_key: Not a path to a file, or it doesn't exist.", ctx) + if not group_certificate.is_file(): + raise click.UsageError("group_certificate: Not a path to a file, or it doesn't exist.", ctx) + + log = logging.getLogger("create-device") + + encryption_key = ECCKey.load(encryption_key) if encryption_key else ECCKey.generate() + signing_key = ECCKey.load(signing_key) if signing_key else ECCKey.generate() + + group_key = ECCKey.load(group_key) + certificate_chain = CertificateChain.load(group_certificate) + + if certificate_chain.get(0).get_issuer_key() != group_key.public_bytes(): + raise InvalidCertificateChain("Group key does not match this certificate") + + new_certificate = Certificate.new_leaf_cert( + cert_id=get_random_bytes(16), + security_level=certificate_chain.get_security_level(), + client_id=get_random_bytes(16), + signing_key=signing_key, + encryption_key=encryption_key, + group_key=group_key, + parent=certificate_chain + ) + certificate_chain.prepend(new_certificate) + + certificate_chain.verify() + + device = Device( + group_key=group_key.dumps(), + encryption_key=encryption_key.dumps(), + signing_key=signing_key.dumps(), + group_certificate=certificate_chain.dumps(), + ) + + if output and output.suffix: + if output.suffix.lower() != ".prd": + log.warning(f"Saving PRD with the file extension '{output.suffix}' but '.prd' is recommended.") + out_path = output + else: + out_dir = output or Path.cwd() + out_path = out_dir / f"{device.get_name()}.prd" + + if out_path.exists(): + log.error(f"A file already exists at the path '{out_path}', cannot overwrite.") + return + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_bytes(device.dumps()) + + log.info("Created Playready Device (.prd) file, %s", out_path.name) + log.info(" + Security Level: %s", device.security_level) + log.info(" + Group Key: %s bytes", len(device.group_key.dumps())) + log.info(" + Encryption Key: %s bytes", len(device.encryption_key.dumps())) + log.info(" + Signing Key: %s bytes", len(device.signing_key.dumps())) + log.info(" + Group Certificate: %s bytes", len(device.group_certificate.dumps())) + log.info(" + Saved to: %s", out_path.absolute()) + + +@main.command() +@click.argument("prd_path", type=Path) +@click.option("-e", "--encryption_key", type=Path, required=False, help="Optional Device ECC private encryption key") +@click.option("-s", "--signing_key", type=Path, required=False, help="Optional Device ECC private signing key") +@click.option("-o", "--output", type=Path, default=None, help="Output Path or Directory") +@click.pass_context +def reprovision_device( + ctx: click.Context, + prd_path: Path, + encryption_key: Optional[Path], + signing_key: Optional[Path], + output: Optional[Path] = None +) -> None: + """ + Reprovision a Playready Device (.prd) by creating a new leaf certificate and new encryption/signing keys. + Will override the device if an output path or directory is not specified + + Only works on PRD Devices of v3 or higher + """ + if not prd_path.is_file(): + raise click.UsageError("prd_path: Not a path to a file, or it doesn't exist.", ctx) + + log = logging.getLogger("reprovision-device") + log.info("Reprovisioning Playready Device (.prd) file, %s", prd_path.name) + + device = Device.load(prd_path) + + if device.group_key is None: + raise OutdatedDevice("Device does not support reprovisioning, re-create it or use a Device with a version of 3 or higher") + + device.group_certificate.remove(0) + + encryption_key = ECCKey.load(encryption_key) if encryption_key else ECCKey.generate() + signing_key = ECCKey.load(signing_key) if signing_key else ECCKey.generate() + + device.encryption_key = encryption_key + device.signing_key = signing_key + + new_certificate = Certificate.new_leaf_cert( + cert_id=get_random_bytes(16), + security_level=device.group_certificate.get_security_level(), + client_id=get_random_bytes(16), + signing_key=signing_key, + encryption_key=encryption_key, + group_key=device.group_key, + parent=device.group_certificate + ) + device.group_certificate.prepend(new_certificate) + + if output and output.suffix: + if output.suffix.lower() != ".prd": + log.warning(f"Saving PRD with the file extension '{output.suffix}' but '.prd' is recommended.") + out_path = output + else: + out_path = prd_path + + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_bytes(device.dumps()) + + log.info("Reprovisioned Playready Device (.prd) file, %s", out_path.name) + + +@main.command() +@click.argument("prd_path", type=Path) +@click.option("-o", "--out_dir", type=Path, default=None, help="Output Directory") +@click.pass_context +def export_device(ctx: click.Context, prd_path: Path, out_dir: Optional[Path] = None) -> None: + """ + Export a Playready Device (.prd) file to a Group Key and Group Certificate + If an output directory is not specified, it will be stored in the current working directory + """ + if not prd_path.is_file(): + raise click.UsageError("prd_path: Not a path to a file, or it doesn't exist.", ctx) + + log = logging.getLogger("export-device") + log.info("Exporting Playready Device (.prd) file, %s", prd_path.stem) + + if not out_dir: + out_dir = Path.cwd() + + out_path = out_dir / prd_path.stem + if out_path.exists(): + if any(out_path.iterdir()): + log.error("Output directory is not empty, cannot overwrite.") + return + else: + log.warning("Output directory already exists, but is empty.") + else: + out_path.mkdir(parents=True) + + device = Device.load(prd_path) + + log.info(f"SL{device.security_level} {device.get_name()}") + log.info(f"Saving to: {out_path}") + + if device.group_key: + group_key_path = out_path / "zgpriv.dat" + group_key_path.write_bytes(device.group_key.dumps(private_only=True)) + log.info("Exported Group Key as zgpriv.dat") + else: + log.warning("Cannot export zgpriv.dat, as v2 devices do not save the group key") + + # remove leaf cert to unprovision it + device.group_certificate.remove(0) + + client_id_path = out_path / "bgroupcert.dat" + client_id_path.write_bytes(device.group_certificate.dumps()) + log.info("Exported Group Certificate to bgroupcert.dat") + + +@main.command("serve", short_help="Serve your local CDM and Playready Devices remotely.") +@click.argument("config_path", type=Path) +@click.option("-h", "--host", type=str, default="127.0.0.1", help="Host to serve from.") +@click.option("-p", "--port", type=int, default=7723, help="Port to serve from.") +def serve_(config_path: Path, host: str, port: int) -> None: + """ + Serve your local CDM and Playready Devices Remotely. + + [CONFIG] is a path to a serve config file. + See `serve.example.yml` for an example config file. + + Host as 127.0.0.1 may block remote access even if port-forwarded. + Instead, use 0.0.0.0 and ensure the TCP port you choose is forwarded. + """ + from pyplayready.remote import serve + import yaml + + config = yaml.safe_load(config_path.read_text(encoding="utf8")) + serve.run(config, host, port) diff --git a/pyplayready/remote/remotecdm.py b/pyplayready/remote/remotecdm.py new file mode 100644 index 0000000..404858f --- /dev/null +++ b/pyplayready/remote/remotecdm.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import logging +from typing import Union + +import requests + +from pyplayready import InvalidLicense +from pyplayready.cdm import Cdm +from pyplayready.device import Device +from pyplayready.license.key import Key + +from pyplayready.exceptions import (DeviceMismatch, InvalidInitData) +from pyplayready.system.wrmheader import WRMHeader + + +class RemoteCdm(Cdm): + """Remote Accessible CDM using pyplayready's serve schema.""" + + def __init__( + self, + security_level: int, + host: str, + secret: str, + device_name: str + ): + """Initialize a Playready Content Decryption Module (CDM).""" + if not security_level: + raise ValueError("Security Level must be provided") + if not isinstance(security_level, int): + raise TypeError(f"Expected security_level to be a {int} not {security_level!r}") + + if not host: + raise ValueError("API Host must be provided") + if not isinstance(host, str): + raise TypeError(f"Expected host to be a {str} not {host!r}") + + if not secret: + raise ValueError("API Secret must be provided") + if not isinstance(secret, str): + raise TypeError(f"Expected secret to be a {str} not {secret!r}") + + if not device_name: + raise ValueError("API Device name must be provided") + if not isinstance(device_name, str): + raise TypeError(f"Expected device_name to be a {str} not {device_name!r}") + + self.security_level = security_level + self.host = host + self.device_name = device_name + + # spoof certificate_chain and ecc_key just so we can construct via super call + super().__init__(security_level, None, None, None) + + self._logger = logging.getLogger() + + self.__session = requests.Session() + self.__session.headers.update({ + "X-Secret-Key": secret + }) + + response = requests.head(self.host) + + if response.status_code != 200: + self._logger.warning(f"Could not test Remote API version [{response.status_code}]") + + server = response.headers.get("Server") + if not server or "playready serve" not in server.lower(): + self._logger.warning(f"This Remote CDM API does not seem to be a playready serve API ({server}).") + + @classmethod + def from_device(cls, device: Device) -> RemoteCdm: + raise NotImplementedError("You cannot load a RemoteCdm from a local Device file.") + + def open(self) -> bytes: + response = self.__session.get( + url=f"{self.host}/{self.device_name}/open" + ) + response_json = response.json() + + if response.status_code != 200: + raise ValueError(f"Cannot Open CDM Session, {response_json['message']} [{response.status_code}]") + + if int(response_json["data"]["device"]["security_level"]) != self.security_level: + raise DeviceMismatch("The Security Level specified does not match the one specified in the API response.") + + return bytes.fromhex(response_json["data"]["session_id"]) + + def close(self, session_id: bytes) -> None: + response = self.__session.get( + url=f"{self.host}/{self.device_name}/close/{session_id.hex()}" + ) + response_json = response.json() + + if response.status_code != 200: + raise ValueError(f"Cannot Close CDM Session, {response_json['message']} [{response.status_code}]") + + def get_license_challenge(self, session_id: bytes, wrm_header: Union[WRMHeader, str]) -> str: + if not wrm_header: + raise InvalidInitData("A wrm_header must be provided.") + if isinstance(wrm_header, WRMHeader): + wrm_header = wrm_header.dumps() + if not isinstance(wrm_header, str): + raise ValueError(f"Expected WRMHeader to be a {str} or {WRMHeader} not {wrm_header!r}") + + response = self.__session.post( + url=f"{self.host}/{self.device_name}/get_license_challenge", + json={ + "session_id": session_id.hex(), + "init_data": wrm_header + } + ) + response_json = response.json() + + if response.status_code != 200: + raise ValueError(f"Cannot get Challenge, {response_json['message']} [{response.status_code}]") + + return response_json["data"]["challenge"] + + def parse_license(self, session_id: bytes, license_message: str) -> None: + if not license_message: + raise InvalidLicense("Cannot parse an empty license_message") + + if not isinstance(license_message, str): + raise InvalidLicense(f"Expected license_message to be a {str}, not {license_message!r}") + + response = self.__session.post( + url=f"{self.host}/{self.device_name}/parse_license", + json={ + "session_id": session_id.hex(), + "license_message": license_message + } + ) + response_json = response.json() + + if response.status_code != 200: + raise ValueError(f"Cannot parse License, {response_json['message']} [{response.status_code}]") + + def get_keys(self, session_id: bytes) -> list[Key]: + response = self.__session.post( + url=f"{self.host}/{self.device_name}/get_keys", + json={ + "session_id": session_id.hex() + } + ) + response_json = response.json() + + if response.status_code != 200: + raise ValueError(f"Could not get Keys, {response_json['message']} [{response.status_code}]") + + return [ + Key( + key_type=key["type"], + key_id=Key.kid_to_uuid(bytes.fromhex(key["key_id"])), + key=bytes.fromhex(key["key"]), + cipher_type=key["cipher_type"], + key_length=key["key_length"] + ) + for key in response_json["data"]["keys"] + ] + + +__all__ = ("RemoteCdm",) diff --git a/pyplayready/remote/serve.py b/pyplayready/remote/serve.py new file mode 100644 index 0000000..1941a6f --- /dev/null +++ b/pyplayready/remote/serve.py @@ -0,0 +1,234 @@ +from pathlib import Path +from typing import Any, Optional, Union + +from aiohttp.typedefs import Handler +from aiohttp import web + +from pyplayready import __version__, PSSH +from pyplayready.cdm import Cdm +from pyplayready.device import Device + +from pyplayready.exceptions import (InvalidSession, TooManySessions, InvalidLicense, InvalidPssh) + +routes = web.RouteTableDef() + + +async def _startup(app: web.Application) -> None: + app["cdms"] = {} + app["config"]["devices"] = { + path.stem: path + for x in app["config"]["devices"] + for path in [Path(x)] + } + for device in app["config"]["devices"].values(): + if not device.is_file(): + raise FileNotFoundError(f"Device file does not exist: {device}") + + +async def _cleanup(app: web.Application) -> None: + app["cdms"].clear() + del app["cdms"] + app["config"].clear() + del app["config"] + + +@routes.get("/") +async def ping(_: Any) -> web.Response: + return web.json_response({"message": "OK"}) + + +@routes.get("/{device}/open") +async def open_(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + user = request.app["config"]["users"][secret_key] + + if device_name not in user["devices"] or device_name not in request.app["config"]["devices"]: + # we don't want to be verbose with the error as to not reveal device names + # by trial and error to users that are not authorized to use them + return web.json_response({"message": f"Device '{device_name}' is not found or you are not authorized to use it."}, status=403) + + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + device = Device.load(request.app["config"]["devices"][device_name]) + cdm = request.app["cdms"][(secret_key, device_name)] = Cdm.from_device(device) + + try: + session_id = cdm.open() + except TooManySessions as e: + return web.json_response({"message": str(e)}, status=400) + + return web.json_response({ + "message": "Success", + "data": { + "session_id": session_id.hex(), + "device": { + "security_level": cdm.security_level + } + } + }) + + +@routes.get("/{device}/close/{session_id}") +async def close(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + session_id = bytes.fromhex(request.match_info["session_id"]) + + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({"message": f"No Cdm session for {device_name} has been opened yet. No session to close."}, status=400) + + try: + cdm.close(session_id) + except InvalidSession: + return web.json_response({"message": f"Invalid Session ID '{session_id.hex()}', it may have expired."}, status=400) + + return web.json_response({"message": f"Successfully closed Session '{session_id.hex()}'."}) + + +@routes.post("/{device}/get_license_challenge") +async def get_license_challenge(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("init_data", "session_id"): + if not body.get(required_field): + return web.json_response({"message": f"Missing required field '{required_field}' in JSON body."}, status=400) + + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({"message": f"No Cdm session for {device_name} has been opened yet. No session to use."}, status=400) + + session_id = bytes.fromhex(body["session_id"]) + init_data = body["init_data"] + + if not init_data.startswith(" web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("license_message", "session_id"): + if not body.get(required_field): + return web.json_response({"message": f"Missing required field '{required_field}' in JSON body."}, status=400) + + cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({"message": f"No Cdm session for {device_name} has been opened yet. No session to use."}, status=400) + + session_id = bytes.fromhex(body["session_id"]) + license_message = body["license_message"] + + try: + cdm.parse_license(session_id, license_message) + except InvalidSession: + return web.json_response({"message": f"Invalid Session ID '{session_id.hex()}', it may have expired."}, status=400) + except InvalidLicense as e: + return web.json_response({"message": f"Invalid License, {e}"}, status=400) + except Exception as e: + return web.json_response({"message": f"Error, {e}"}, status=500) + + return web.json_response({"message": "Successfully parsed and loaded the Keys from the License message."}) + + +@routes.post("/{device}/get_keys") +async def get_keys(request: web.Request) -> web.Response: + secret_key = request.headers["X-Secret-Key"] + device_name = request.match_info["device"] + + body = await request.json() + for required_field in ("session_id",): + if not body.get(required_field): + return web.json_response({"message": f"Missing required field '{required_field}' in JSON body."}, status=400) + + session_id = bytes.fromhex(body["session_id"]) + + cdm = request.app["cdms"].get((secret_key, device_name)) + if not cdm: + return web.json_response({"message": f"No Cdm session for {device_name} has been opened yet. No session to use."}, status=400) + + try: + keys = cdm.get_keys(session_id) + except InvalidSession: + return web.json_response({"message": f"Invalid Session ID '{session_id.hex()}', it may have expired."}, status=400) + except Exception as e: + return web.json_response({"message": f"Error, {e}"}, status=500) + + keys_json = [ + { + "key_id": key.key_id.hex, + "key": key.key.hex(), + "type": key.key_type.value, + "cipher_type": key.cipher_type.value, + "key_length": key.key_length, + } + for key in keys + ] + + return web.json_response({ + "message": "Success", + "data": { + "keys": keys_json + } + }) + + +@web.middleware +async def authentication(request: web.Request, handler: Handler) -> web.Response: + secret_key = request.headers.get("X-Secret-Key") + + if request.path != "/" and not secret_key: + request.app.logger.debug(f"{request.remote} did not provide authorization.") + response = web.json_response({"message": "Secret Key is Empty."}, status=401) + elif request.path != "/" and secret_key not in request.app["config"]["users"]: + request.app.logger.debug(f"{request.remote} failed authentication with '{secret_key}'.") + response = web.json_response({"message": "Secret Key is Invalid, the Key is case-sensitive."}, status=401) + else: + try: + response = await handler(request) + except web.HTTPException as e: + request.app.logger.error(f"An unexpected error has occurred, {e}") + response = web.json_response({"message": e.reason}, status=500) + + response.headers.update({ + "Server": f"https://github.com/ready-dl/pyplayready serve v{__version__}" + }) + + return response + + +def run(config: dict, host: Optional[Union[str, web.HostSequence]] = None, port: Optional[int] = None) -> None: + app = web.Application(middlewares=[authentication]) + app.on_startup.append(_startup) + app.on_cleanup.append(_cleanup) + app.add_routes(routes) + app["config"] = config + web.run_app(app, host=host, port=port) diff --git a/pyplayready/system/bcert.py b/pyplayready/system/bcert.py new file mode 100644 index 0000000..293cce7 --- /dev/null +++ b/pyplayready/system/bcert.py @@ -0,0 +1,594 @@ +from __future__ import annotations +import collections.abc + +# monkey patch for construct 2.8.8 compatibility +if not hasattr(collections, 'Sequence'): + collections.Sequence = collections.abc.Sequence + +import base64 +from pathlib import Path +from typing import Union, Optional +from enum import IntEnum + +from Crypto.PublicKey import ECC + +from construct import Bytes, Const, Int32ub, GreedyRange, Switch, Container, ListContainer +from construct import Int16ub, Array +from construct import Struct, this + +from pyplayready.crypto import Crypto +from pyplayready.exceptions import InvalidCertificateChain, InvalidCertificate +from pyplayready.crypto.ecc_key import ECCKey + + +class BCertCertType(IntEnum): + UNKNOWN = 0x00000000 + PC = 0x00000001 + DEVICE = 0x00000002 + DOMAIN = 0x00000003 + ISSUER = 0x00000004 + CRL_SIGNER = 0x00000005 + SERVICE = 0x00000006 + SILVERLIGHT = 0x00000007 + APPLICATION = 0x00000008 + METERING = 0x00000009 + KEYFILESIGNER = 0x0000000a + SERVER = 0x0000000b + LICENSESIGNER = 0x0000000c + SECURETIMESERVER = 0x0000000d + RPROVMODELAUTH = 0x0000000e + + +class BCertObjType(IntEnum): + BASIC = 0x0001 + DOMAIN = 0x0002 + PC = 0x0003 + DEVICE = 0x0004 + FEATURE = 0x0005 + KEY = 0x0006 + MANUFACTURER = 0x0007 + SIGNATURE = 0x0008 + SILVERLIGHT = 0x0009 + METERING = 0x000A + EXTDATASIGNKEY = 0x000B + EXTDATACONTAINER = 0x000C + EXTDATASIGNATURE = 0x000D + EXTDATA_HWID = 0x000E + SERVER = 0x000F + SECURITY_VERSION = 0x0010 + SECURITY_VERSION_2 = 0x0011 + UNKNOWN_OBJECT_ID = 0xFFFD + + +class BCertFlag(IntEnum): + EMPTY = 0x00000000 + EXTDATA_PRESENT = 0x00000001 + + +class BCertObjFlag(IntEnum): + EMPTY = 0x0000 + MUST_UNDERSTAND = 0x0001 + CONTAINER_OBJ = 0x0002 + + +class BCertSignatureType(IntEnum): + P256 = 0x0001 + + +class BCertKeyType(IntEnum): + ECC256 = 0x0001 + + +class BCertKeyUsage(IntEnum): + UNKNOWN = 0x00000000 + SIGN = 0x00000001 + ENCRYPT_KEY = 0x00000002 + SIGN_CRL = 0x00000003 + ISSUER_ALL = 0x00000004 + ISSUER_INDIV = 0x00000005 + ISSUER_DEVICE = 0x00000006 + ISSUER_LINK = 0x00000007 + ISSUER_DOMAIN = 0x00000008 + ISSUER_SILVERLIGHT = 0x00000009 + ISSUER_APPLICATION = 0x0000000a + ISSUER_CRL = 0x0000000b + ISSUER_METERING = 0x0000000c + ISSUER_SIGN_KEYFILE = 0x0000000d + SIGN_KEYFILE = 0x0000000e + ISSUER_SERVER = 0x0000000f + ENCRYPTKEY_SAMPLE_PROTECTION_RC4 = 0x00000010 + RESERVED2 = 0x00000011 + ISSUER_SIGN_LICENSE = 0x00000012 + SIGN_LICENSE = 0x00000013 + SIGN_RESPONSE = 0x00000014 + PRND_ENCRYPT_KEY_DEPRECATED = 0x00000015 + ENCRYPTKEY_SAMPLE_PROTECTION_AES128CTR = 0x00000016 + ISSUER_SECURETIMESERVER = 0x00000017 + ISSUER_RPROVMODELAUTH = 0x00000018 + + +class BCertFeatures(IntEnum): + TRANSMITTER = 0x00000001 + RECEIVER = 0x00000002 + SHARED_CERTIFICATE = 0x00000003 + SECURE_CLOCK = 0x00000004 + ANTIROLLBACK_CLOCK = 0x00000005 + RESERVED_METERING = 0x00000006 + RESERVED_LICSYNC = 0x00000007 + RESERVED_SYMOPT = 0x00000008 + SUPPORTS_CRLS = 0x00000009 + SERVER_BASIC_EDITION = 0x0000000A + SERVER_STANDARD_EDITION = 0x0000000B + SERVER_PREMIUM_EDITION = 0x0000000C + SUPPORTS_PR3_FEATURES = 0x0000000D + DEPRECATED_SECURE_STOP = 0x0000000E + + +class _BCertStructs: + BasicInfo = Struct( + "cert_id" / Bytes(16), + "security_level" / Int32ub, + "flags" / Int32ub, + "cert_type" / Int32ub, + "public_key_digest" / Bytes(32), + "expiration_date" / Int32ub, + "client_id" / Bytes(16) + ) + + # TODO: untested + DomainInfo = Struct( + "service_id" / Bytes(16), + "account_id" / Bytes(16), + "revision_timestamp" / Int32ub, + "domain_url_length" / Int32ub, + "domain_url" / Bytes((this.domain_url_length + 3) & 0xfffffffc) + ) + + # TODO: untested + PCInfo = Struct( + "security_version" / Int32ub + ) + + DeviceInfo = Struct( + "max_license" / Int32ub, + "max_header" / Int32ub, + "max_chain_depth" / Int32ub + ) + + FeatureInfo = Struct( + "feature_count" / Int32ub, # max. 32 + "features" / Array(this.feature_count, Int32ub) + ) + + KeyInfo = Struct( + "key_count" / Int32ub, + "cert_keys" / Array(this.key_count, Struct( + "type" / Int16ub, + "length" / Int16ub, + "flags" / Int32ub, + "key" / Bytes(this.length // 8), + "usages_count" / Int32ub, + "usages" / Array(this.usages_count, Int32ub) + )) + ) + + ManufacturerInfo = Struct( + "flags" / Int32ub, + "manufacturer_name_length" / Int32ub, + "manufacturer_name" / Bytes((this.manufacturer_name_length + 3) & 0xfffffffc), + "model_name_length" / Int32ub, + "model_name" / Bytes((this.model_name_length + 3) & 0xfffffffc), + "model_number_length" / Int32ub, + "model_number" / Bytes((this.model_number_length + 3) & 0xfffffffc), + ) + + SignatureInfo = Struct( + "signature_type" / Int16ub, + "signature_size" / Int16ub, + "signature" / Bytes(this.signature_size), + "signature_key_size" / Int32ub, + "signature_key" / Bytes(this.signature_key_size // 8) + ) + + # TODO: untested + SilverlightInfo = Struct( + "security_version" / Int32ub, + "platform_identifier" / Int32ub + ) + + # TODO: untested + MeteringInfo = Struct( + "metering_id" / Bytes(16), + "metering_url_length" / Int32ub, + "metering_url" / Bytes((this.metering_url_length + 3) & 0xfffffffc) + ) + + ExtDataSignKeyInfo = Struct( + "key_type" / Int16ub, + "key_length" / Int16ub, + "flags" / Int32ub, + "key" / Bytes(this.key_length // 8) + ) + + # TODO: untested + DataRecord = Struct( + "data_size" / Int32ub, + "data" / Bytes(this.data_size) + ) + + ExtDataSignature = Struct( + "signature_type" / Int16ub, + "signature_size" / Int16ub, + "signature" / Bytes(this.signature_size) + ) + + ExtDataContainer = Struct( + "record_count" / Int32ub, # always 1 + "records" / Array(this.record_count, DataRecord), + "signature" / ExtDataSignature + ) + + # TODO: untested + ServerInfo = Struct( + "warning_days" / Int32ub + ) + + # TODO: untested + SecurityVersion = Struct( + "security_version" / Int32ub, + "platform_identifier" / Int32ub + ) + + Attribute = Struct( + "flags" / Int16ub, + "tag" / Int16ub, + "length" / Int32ub, + "attribute" / Switch( + lambda this_: this_.tag, + { + BCertObjType.BASIC: BasicInfo, + BCertObjType.DOMAIN: DomainInfo, + BCertObjType.PC: PCInfo, + BCertObjType.DEVICE: DeviceInfo, + BCertObjType.FEATURE: FeatureInfo, + BCertObjType.KEY: KeyInfo, + BCertObjType.MANUFACTURER: ManufacturerInfo, + BCertObjType.SIGNATURE: SignatureInfo, + BCertObjType.SILVERLIGHT: SilverlightInfo, + BCertObjType.METERING: MeteringInfo, + BCertObjType.EXTDATASIGNKEY: ExtDataSignKeyInfo, + BCertObjType.EXTDATACONTAINER: ExtDataContainer, + BCertObjType.EXTDATASIGNATURE: ExtDataSignature, + BCertObjType.EXTDATA_HWID: Bytes(this.length - 8), + BCertObjType.SERVER: ServerInfo, + BCertObjType.SECURITY_VERSION: SecurityVersion, + BCertObjType.SECURITY_VERSION_2: SecurityVersion + }, + default=Bytes(this.length - 8) + ) + ) + + BCert = Struct( + "signature" / Const(b"CERT"), + "version" / Int32ub, + "total_length" / Int32ub, + "certificate_length" / Int32ub, + "attributes" / GreedyRange(Attribute) + ) + + BCertChain = Struct( + "signature" / Const(b"CHAI"), + "version" / Int32ub, + "total_length" / Int32ub, + "flags" / Int32ub, + "certificate_count" / Int32ub, + "certificates" / GreedyRange(BCert) + ) + + +class Certificate(_BCertStructs): + """Represents a BCert""" + + def __init__( + self, + parsed_bcert: Container, + bcert_obj: _BCertStructs.BCert = _BCertStructs.BCert + ): + self.parsed = parsed_bcert + self._BCERT = bcert_obj + + @classmethod + def new_leaf_cert( + cls, + cert_id: bytes, + security_level: int, + client_id: bytes, + signing_key: ECCKey, + encryption_key: ECCKey, + group_key: ECCKey, + parent: CertificateChain, + expiry: int = 0xFFFFFFFF + ) -> Certificate: + basic_info = Container( + cert_id=cert_id, + security_level=security_level, + flags=BCertFlag.EMPTY, + cert_type=BCertCertType.DEVICE, + public_key_digest=signing_key.public_sha256_digest(), + expiration_date=expiry, + client_id=client_id + ) + basic_info_attribute = Container( + flags=BCertObjFlag.MUST_UNDERSTAND, + tag=BCertObjType.BASIC, + length=len(_BCertStructs.BasicInfo.build(basic_info)) + 8, + attribute=basic_info + ) + + device_info = Container( + max_license=10240, + max_header=15360, + max_chain_depth=2 + ) + device_info_attribute = Container( + flags=BCertObjFlag.MUST_UNDERSTAND, + tag=BCertObjType.DEVICE, + length=len(_BCertStructs.DeviceInfo.build(device_info)) + 8, + attribute=device_info + ) + + feature = Container( + feature_count=3, + features=ListContainer([ + BCertFeatures.SECURE_CLOCK, + BCertFeatures.SUPPORTS_CRLS, + BCertFeatures.SUPPORTS_PR3_FEATURES + ]) + ) + feature_attribute = Container( + flags=BCertObjFlag.MUST_UNDERSTAND, + tag=BCertObjType.FEATURE, + length=len(_BCertStructs.FeatureInfo.build(feature)) + 8, + attribute=feature + ) + + signing_key_public_bytes = signing_key.public_bytes() + cert_key_sign = Container( + type=BCertKeyType.ECC256, + length=len(signing_key_public_bytes) * 8, # bits + flags=BCertFlag.EMPTY, + key=signing_key_public_bytes, + usages_count=1, + usages=ListContainer([ + BCertKeyUsage.SIGN + ]) + ) + + encryption_key_public_bytes = encryption_key.public_bytes() + cert_key_encrypt = Container( + type=BCertKeyType.ECC256, + length=len(encryption_key_public_bytes) * 8, # bits + flags=BCertFlag.EMPTY, + key=encryption_key_public_bytes, + usages_count=1, + usages=ListContainer([ + BCertKeyUsage.ENCRYPT_KEY + ]) + ) + + key_info = Container( + key_count=2, + cert_keys=ListContainer([ + cert_key_sign, + cert_key_encrypt + ]) + ) + key_info_attribute = Container( + flags=BCertObjFlag.MUST_UNDERSTAND, + tag=BCertObjType.KEY, + length=len(_BCertStructs.KeyInfo.build(key_info)) + 8, + attribute=key_info + ) + + manufacturer_info = parent.get(0).get_attribute(BCertObjType.MANUFACTURER) + + new_bcert_container = Container( + signature=b"CERT", + version=1, + total_length=0, # filled at a later time + certificate_length=0, # filled at a later time + attributes=ListContainer([ + basic_info_attribute, + device_info_attribute, + feature_attribute, + key_info_attribute, + manufacturer_info, + ]) + ) + + payload = _BCertStructs.BCert.build(new_bcert_container) + new_bcert_container.certificate_length = len(payload) + new_bcert_container.total_length = len(payload) + 144 # signature length + + sign_payload = _BCertStructs.BCert.build(new_bcert_container) + signature = Crypto.ecc256_sign(group_key, sign_payload) + + group_key_public_bytes = group_key.public_bytes() + + signature_info = Container( + signature_type=BCertSignatureType.P256, + signature_size=len(signature), + signature=signature, + signature_key_size=len(group_key_public_bytes) * 8, # bits + signature_key=group_key_public_bytes + ) + signature_info_attribute = Container( + flags=BCertObjFlag.MUST_UNDERSTAND, + tag=BCertObjType.SIGNATURE, + length=len(_BCertStructs.SignatureInfo.build(signature_info)) + 8, + attribute=signature_info + ) + new_bcert_container.attributes.append(signature_info_attribute) + + return cls(new_bcert_container) + + @classmethod + def loads(cls, data: Union[str, bytes]) -> Certificate: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + cert = _BCertStructs.BCert + return cls( + parsed_bcert=cert.parse(data), + bcert_obj=cert + ) + + def get_attribute(self, type_: int): + for attribute in self.parsed.attributes: + if attribute.tag == type_: + return attribute + + def get_security_level(self) -> int: + basic_info_attribute = self.get_attribute(BCertObjType.BASIC).attribute + if basic_info_attribute: + return basic_info_attribute.security_level + + @staticmethod + def _unpad(name: bytes): + return name.rstrip(b'\x00').decode("utf-8", errors="ignore") + + def get_name(self): + manufacturer_info = self.get_attribute(BCertObjType.MANUFACTURER).attribute + if manufacturer_info: + return f"{self._unpad(manufacturer_info.manufacturer_name)} {self._unpad(manufacturer_info.model_name)} {self._unpad(manufacturer_info.model_number)}" + + def get_issuer_key(self) -> Optional[bytes]: + key_info_object = self.get_attribute(BCertObjType.KEY) + if not key_info_object: + return + + key_info_attribute = key_info_object.attribute + return next(map(lambda key: key.key, filter(lambda key: 6 in key.usages, key_info_attribute.cert_keys)), None) + + def dumps(self) -> bytes: + return self._BCERT.build(self.parsed) + + def verify(self, public_key: bytes, index: int): + signature_object = self.get_attribute(BCertObjType.SIGNATURE) + if not signature_object: + raise InvalidCertificate(f"No signature object found in certificate {index}") + + signature_attribute = signature_object.attribute + + raw_signature_key = signature_attribute.signature_key + if public_key != raw_signature_key: + raise InvalidCertificate(f"Signature keys of certificate {index} do not match") + + signature_key = ECC.construct( + curve='P-256', + point_x=int.from_bytes(raw_signature_key[:32], 'big'), + point_y=int.from_bytes(raw_signature_key[32:], 'big') + ) + + sign_payload = self.dumps()[:-signature_object.length] + + if not Crypto.ecc256_verify( + public_key=signature_key, + data=sign_payload, + signature=signature_attribute.signature + ): + raise InvalidCertificate(f"Signature of certificate {index} is not authentic") + + return self.get_issuer_key() + + +class CertificateChain(_BCertStructs): + """Represents a BCertChain""" + + ECC256MSBCertRootIssuerPubKey = bytes.fromhex("864d61cff2256e422c568b3c28001cfb3e1527658584ba0521b79b1828d936de1d826a8fc3e6e7fa7a90d5ca2946f1f64a2efb9f5dcffe7e434eb44293fac5ab") + + def __init__( + self, + parsed_bcert_chain: Container, + bcert_chain_obj: _BCertStructs.BCertChain = _BCertStructs.BCertChain + ): + self.parsed = parsed_bcert_chain + self._BCERT_CHAIN = bcert_chain_obj + + @classmethod + def loads(cls, data: Union[str, bytes]) -> CertificateChain: + if isinstance(data, str): + data = base64.b64decode(data) + if not isinstance(data, bytes): + raise ValueError(f"Expecting Bytes or Base64 input, got {data!r}") + + cert_chain = _BCertStructs.BCertChain + return cls( + parsed_bcert_chain=cert_chain.parse(data), + bcert_chain_obj=cert_chain + ) + + @classmethod + def load(cls, path: Union[Path, str]) -> CertificateChain: + if not isinstance(path, (Path, str)): + raise ValueError(f"Expecting Path object or path string, got {path!r}") + with Path(path).open(mode="rb") as f: + return cls.loads(f.read()) + + def dumps(self) -> bytes: + return self._BCERT_CHAIN.build(self.parsed) + + def get_security_level(self) -> int: + # not sure if there's a better way than this + return self.get(0).get_security_level() + + def get_name(self) -> str: + return self.get(0).get_name() + + def verify(self) -> bool: + issuer_key = self.ECC256MSBCertRootIssuerPubKey + + try: + for i in reversed(range(self.count())): + certificate = self.get(i) + issuer_key = certificate.verify(issuer_key, i) + + if not issuer_key and i != 0: + raise InvalidCertificate(f"Certificate {i} is not valid") + except InvalidCertificate as e: + raise InvalidCertificateChain(e) + + return True + + def append(self, bcert: Certificate) -> None: + self.parsed.certificate_count += 1 + self.parsed.certificates.append(bcert.parsed) + self.parsed.total_length += len(bcert.dumps()) + + def prepend(self, bcert: Certificate) -> None: + self.parsed.certificate_count += 1 + self.parsed.certificates.insert(0, bcert.parsed) + self.parsed.total_length += len(bcert.dumps()) + + def remove(self, index: int) -> None: + if self.count() <= 0: + raise InvalidCertificateChain("CertificateChain does not contain any Certificates") + if index >= self.count(): + raise IndexError(f"No Certificate at index {index}, {self.count()} total") + + self.parsed.certificate_count -= 1 + self.parsed.total_length -= len(self.get(index).dumps()) + self.parsed.certificates.pop(index) + + def get(self, index: int) -> Certificate: + if self.count() <= 0: + raise InvalidCertificateChain("CertificateChain does not contain any Certificates") + if index >= self.count(): + raise IndexError(f"No Certificate at index {index}, {self.count()} total") + + return Certificate(self.parsed.certificates[index]) + + def count(self) -> int: + return self.parsed.certificate_count diff --git a/pyplayready/system/pssh.py b/pyplayready/system/pssh.py new file mode 100644 index 0000000..760c01b --- /dev/null +++ b/pyplayready/system/pssh.py @@ -0,0 +1,108 @@ +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.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 + ) + )) diff --git a/pyplayready/system/session.py b/pyplayready/system/session.py new file mode 100644 index 0000000..59f0570 --- /dev/null +++ b/pyplayready/system/session.py @@ -0,0 +1,20 @@ +from typing import Optional + +from Crypto.Random import get_random_bytes + +from pyplayready.license.key import Key +from pyplayready.crypto.ecc_key import ECCKey +from pyplayready.license.xml_key import XmlKey + + +class Session: + def __init__(self, number: int): + self.number = number + self.id = get_random_bytes(16) + self.xml_key = XmlKey() + self.signing_key: Optional[ECCKey] = None + self.encryption_key: Optional[ECCKey] = None + self.keys: list[Key] = [] + + +__all__ = ("Session",) diff --git a/pyplayready/system/wrmheader.py b/pyplayready/system/wrmheader.py new file mode 100644 index 0000000..292c0a3 --- /dev/null +++ b/pyplayready/system/wrmheader.py @@ -0,0 +1,145 @@ +import base64 +from enum import Enum +from typing import Optional, List, Union, Tuple + +import xmltodict + + +class WRMHeader: + """Represents a PlayReady WRM Header""" + + class SignedKeyID: + def __init__( + self, + alg_id: str, + value: str, + checksum: str + ): + self.alg_id = alg_id + self.value = value + self.checksum = checksum + + def __repr__(self): + return f'SignedKeyID(alg_id={self.alg_id}, value="{self.value}", checksum="{self.checksum}")' + + class Version(Enum): + VERSION_4_0_0_0 = "4.0.0.0" + VERSION_4_1_0_0 = "4.1.0.0" + VERSION_4_2_0_0 = "4.2.0.0" + VERSION_4_3_0_0 = "4.3.0.0" + UNKNOWN = "UNKNOWN" + + @classmethod + def _missing_(cls, value): + return cls.UNKNOWN + + _RETURN_STRUCTURE = Tuple[List[SignedKeyID], Optional[str], Optional[str], Optional[str]] + + def __init__(self, data: Union[str, bytes]): + """Load a WRM Header from either a string, base64 encoded data or bytes""" + + if not data: + raise ValueError("Data must not be empty") + + if isinstance(data, str): + try: + data = base64.b64decode(data).decode() + except Exception: + data = data.encode("utf-16-le") + + self._raw_data: bytes = data + self._parsed = xmltodict.parse(self._raw_data) + + self._header = self._parsed.get('WRMHEADER') + if not self._header: + raise ValueError("Data is not a valid WRMHEADER") + + self.version = self.Version(self._header.get('@version')) + + @staticmethod + def _ensure_list(element: Union[dict, list]) -> List: + if isinstance(element, dict): + return [element] + return element + + @staticmethod + def _read_v4_0_0_0(data: dict) -> _RETURN_STRUCTURE: + protect_info = data.get("PROTECTINFO") + + return ( + [WRMHeader.SignedKeyID( + alg_id=protect_info["ALGID"], + value=data["KID"], + checksum=data.get("CHECKSUM") + )], + data.get("LA_URL"), + data.get("LUI_URL"), + data.get("DS_ID") + ) + + @staticmethod + def _read_v4_1_0_0(data: dict) -> _RETURN_STRUCTURE: + protect_info = data.get("PROTECTINFO") + + key_ids = [] + if protect_info: + kid = protect_info["KID"] + if kid: + key_ids = [WRMHeader.SignedKeyID( + alg_id=kid["@ALGID"], + value=kid["@VALUE"], + checksum=kid.get("@CHECKSUM") + )] + + return key_ids, data.get("LA_URL"), data.get("LUI_URL"), data.get("DS_ID") + + @staticmethod + def _read_v4_2_0_0(data: dict) -> _RETURN_STRUCTURE: + protect_info = data.get("PROTECTINFO") + + key_ids = [] + if protect_info: + kids = protect_info["KIDS"] + if kids: + for kid in WRMHeader._ensure_list(kids["KID"]): + key_ids.append(WRMHeader.SignedKeyID( + alg_id=kid["@ALGID"], + value=kid["@VALUE"], + checksum=kid.get("@CHECKSUM") + )) + + return key_ids, data.get("LA_URL"), data.get("LUI_URL"), data.get("DS_ID") + + @staticmethod + def _read_v4_3_0_0(data: dict) -> _RETURN_STRUCTURE: + protect_info = data.get("PROTECTINFO") + + key_ids = [] + if protect_info: + kids = protect_info["KIDS"] + for kid in WRMHeader._ensure_list(kids["KID"]): + key_ids.append(WRMHeader.SignedKeyID( + alg_id=kid.get("@ALGID"), + value=kid["@VALUE"], + checksum=kid.get("@CHECKSUM") + )) + + return key_ids, data.get("LA_URL"), data.get("LUI_URL"), data.get("DS_ID") + + def read_attributes(self) -> _RETURN_STRUCTURE: + """Read any non-custom XML attributes""" + data = self._header.get("DATA") + if not data: + raise ValueError("Not a valid PlayReady Header Record, WRMHEADER/DATA required") + + if self.version == self.Version.VERSION_4_0_0_0: + return self._read_v4_0_0_0(data) + elif self.version == self.Version.VERSION_4_1_0_0: + return self._read_v4_1_0_0(data) + elif self.version == self.Version.VERSION_4_2_0_0: + return self._read_v4_2_0_0(data) + elif self.version == self.Version.VERSION_4_3_0_0: + return self._read_v4_3_0_0(data) + + def dumps(self) -> str: + return self._raw_data.decode("utf-16-le") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..11624fc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "pyplayready" +version = "0.6.0" +description = "pyplayready CDM (Content Decryption Module) implementation in Python." +license = "CC BY-NC-ND 4.0" +authors = ["DevLARLEY, Erevoc", "DevataDev"] +readme = "README.md" +repository = "https://github.com/ready-dl/pyplayready" +keywords = ["python", "drm", "playready", "microsoft"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Natural Language :: English", + "Operating System :: OS Independent", + "Topic :: Multimedia :: Video", + "Topic :: Security :: Cryptography", + "Topic :: Software Development :: Libraries :: Python Modules" +] +include = [ + { path = "README.md", format = "sdist" }, + { path = "LICENSE", format = "sdist" }, +] + +[tool.poetry.urls] +"Issues" = "https://github.com/ready-dl/pyplayready/issues" + +[tool.poetry.dependencies] +python = ">=3.8,<4.0" +requests = "^2.32.3" +pycryptodome = "^3.21.0" +construct = "2.8.8" +ECPy = "^1.2.5" +click = "^8.1.7" +xmltodict = "^0.14.2" +PyYAML = "^6.0.1" +aiohttp = {version = "^3.9.1", optional = true} + +[tool.poetry.scripts] +pyplayready = "pyplayready.main:main" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c543673 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +requests +pycryptodome +ecpy +construct==2.8.8 +click +PyYAML +aiohttp +xmltodict \ No newline at end of file diff --git a/serve.example.yml b/serve.example.yml new file mode 100644 index 0000000..ef6f285 --- /dev/null +++ b/serve.example.yml @@ -0,0 +1,15 @@ +# This data serves as an example configuration file for the `serve` command. +# None of the sensitive data should be re-used. + +# List of Playready Device (.prd) file paths to use with serve. +# Note: Each individual user needs explicit permission to use a device listed. +devices: + - 'C:\Users\ready-dl\Documents\PRDs\test_device_001.prd' + +# List of User's by Secret Key. The Secret Key must be supplied by the User to use the API. +users: + fx206W0FaB2O34HzGsgb8rcDe9e3ijsf: # secret key, a-zA-Z-09{32} is recommended, case-sensitive + username: chloe # only for internal logging, user will not see this name + devices: # list of allowed devices by filename + - test_device_001 + # ... \ No newline at end of file