From a6cffe617f16a80ed93ea93ccb119ab4a84d834c Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 6 Feb 2026 07:21:03 +0200 Subject: [PATCH] Upload --- pymonalisa/.pylintrc | 3 + pymonalisa/LICENSE | 403 +++++++++++++++++++++++++ pymonalisa/README.md | 79 +++++ pymonalisa/pymonalisa/__init__.py | 25 ++ pymonalisa/pymonalisa/cdm.py | 436 ++++++++++++++++++++++++++++ pymonalisa/pymonalisa/exceptions.py | 18 ++ pymonalisa/pymonalisa/license.py | 54 ++++ pymonalisa/pymonalisa/main.py | 170 +++++++++++ pymonalisa/pymonalisa/module.py | 116 ++++++++ pymonalisa/pymonalisa/types.py | 23 ++ pymonalisa/pymonalisa/utils.py | 22 ++ pymonalisa/pyproject.toml | 40 +++ pymonalisa/setup.py | 56 ++++ 13 files changed, 1445 insertions(+) create mode 100644 pymonalisa/.pylintrc create mode 100644 pymonalisa/LICENSE create mode 100644 pymonalisa/README.md create mode 100644 pymonalisa/pymonalisa/__init__.py create mode 100644 pymonalisa/pymonalisa/cdm.py create mode 100644 pymonalisa/pymonalisa/exceptions.py create mode 100644 pymonalisa/pymonalisa/license.py create mode 100644 pymonalisa/pymonalisa/main.py create mode 100644 pymonalisa/pymonalisa/module.py create mode 100644 pymonalisa/pymonalisa/types.py create mode 100644 pymonalisa/pymonalisa/utils.py create mode 100644 pymonalisa/pyproject.toml create mode 100644 pymonalisa/setup.py diff --git a/pymonalisa/.pylintrc b/pymonalisa/.pylintrc new file mode 100644 index 0000000..b16fb95 --- /dev/null +++ b/pymonalisa/.pylintrc @@ -0,0 +1,3 @@ +[MESSAGES CONTROL] + +disable=too-few-public-methods,missing-docstring,too-many-arguments,no-member \ No newline at end of file diff --git a/pymonalisa/LICENSE b/pymonalisa/LICENSE new file mode 100644 index 0000000..cfe676c --- /dev/null +++ b/pymonalisa/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/pymonalisa/README.md b/pymonalisa/README.md new file mode 100644 index 0000000..931b01a --- /dev/null +++ b/pymonalisa/README.md @@ -0,0 +1,79 @@ +# pymonalisa +A Python library to decrypt IQIYI DRM License Ticket using the MonaLisa Content Decryption Module. + +## Installation +```shell +pip install pymonalisa +``` + +Run `pymonalisa --help` to view available CLI functions. + +## Devices +To use pymonalisa, you need a MonaLisa device file (.mld). These device files load the WASM module required to process IQIYI DRM License Tickets correctly. + +## License Processing +Process a MonaLisa encoded license and extract decryption keys: +```shell +pymonalisa license "AAAADgMwAADECisAAAdoS7HZAQEASDVEQ0lELVYzLVAxLTIwMjUwNTE5LTE0NDM1Mi1NRDc0N0RBNS0xNzQ0NjQ0OTk3LUExMTJERwEQcMHbn2aue/wLGYFpS2aNngMCADhAACCzbv21P1LuugXC34EhhYOfBQm1K7vNBcxClOM9nTS50AIQxPPew5UYQhKhOg3UrJqXLQQBAwMDADkBADCvjimPScsXNIyb9HxbzkHRB7Bhv3J8Pvpgm54TFhIKSwH32SedaLf7dJ6PRExsjyoBAQECASADBAA4QAAgIreHYYp2nI86fJDaAxR6CJyvM1h+OKXATIn9aj43O2ADEIDB259mrnv8CxmBaUtmjZ4EAQMEBQATARBwwdufZq57/AsZgWlLZo2eAP8GADQBEIDB259mrnv8CxmBaUtmjZ4AIM11WGlDSqV01Aw8PkaTUEgYPVduGGQpxMTnWbNx7qpP" device.mld +``` + +### Options +- `-t, --key-type`: Filter keys by type (CONTENT or FULL). Default: FULL +- `-v, --version`: Print version information + +## Usage +An example code snippet: + +```python +from pymonalisa.cdm import Cdm +from pymonalisa.module import Module +from pymonalisa.license import License +from pymonalisa.types import KeyType + +# Load the MonaLisa device module +module = Module.load("path/to/device.mld") + +# Initialize CDM from module +cdm = Cdm.from_module(module) + +# Open a new session +session_id = cdm.open() + +# Create license object from base64 encoded license data +license_data = "AAAADgMwAADECisAAAdoS7HZAQEASDVEQ0lELVYzLVAxLTIwMjUwNTE5LTE0NDM1Mi1NRDc0N0RBNS0xNzQ0NjQ0OTk3LUExMTJERwEQcMHbn2aue/wLGYFpS2aNngMCADhAACCzbv21P1LuugXC34EhhYOfBQm1K7vNBcxClOM9nTS50AIQxPPew5UYQhKhOg3UrJqXLQQBAwMDADkBADCvjimPScsXNIyb9HxbzkHRB7Bhv3J8Pvpgm54TFhIKSwH32SedaLf7dJ6PRExsjyoBAQECASADBAA4QAAgIreHYYp2nI86fJDaAxR6CJyvM1h+OKXATIn9aj43O2ADEIDB259mrnv8CxmBaUtmjZ4EAQMEBQATARBwwdufZq57/AsZgWlLZo2eAP8GADQBEIDB259mrnv8CxmBaUtmjZ4AIM11WGlDSqV01Aw8PkaTUEgYPVduGGQpxMTnWbNx7qpP" +license_obj = License(license_data) + +# Parse the license +cdm.parse_license(session_id, license_obj) + +# Get content keys +keys = cdm.get_keys(session_id, KeyType.CONTENT) + +# Print extracted keys +for key in keys: + print(f"{key.key.hex()}") # Content key only + # or for full key info: + # print(f"{key.kid.hex()}:{key.key.hex()}") + +# Close the session +cdm.close(session_id) +``` + +## Key Types +- `CONTENT`: Returns only the content decryption KEY +- `FULL`: Returns both Key ID and content key in KID:KEY format + +## Notes +- This implementation is designed specifically for IQIYI DRM content +- The library currently focuses on content key extraction for decrypting IQIYI BBTS streams +- LicenseVersion 3 and below are supported +- Keys are typically returned in hexadecimal format + +## TODO +- Add support for LicenseVersion up to 3 + +## Credits +Thanks to xhlove and duck. + +## License +This project is intended for educational and research purposes only. \ No newline at end of file diff --git a/pymonalisa/pymonalisa/__init__.py b/pymonalisa/pymonalisa/__init__.py new file mode 100644 index 0000000..f199f90 --- /dev/null +++ b/pymonalisa/pymonalisa/__init__.py @@ -0,0 +1,25 @@ +__version__ = "0.1.2" +__authors__ = ["ReiDoBrega", "duck", "xhlove"] + +from .cdm import Cdm +from .module import Module +from .license import License +from .exceptions import ( + MonalisaError, + MonalisaLicenseError, + MonalisaModuleError, + MonalisaSessionError +) +from .types import KeyType, Key + +__all__ = [ + "Cdm", + "Module", + "License", + "Key", + "KeyType", + "MonalisaError", + "MonalisaLicenseError", + "MonalisaModuleError", + "MonalisaSessionError" +] \ No newline at end of file diff --git a/pymonalisa/pymonalisa/cdm.py b/pymonalisa/pymonalisa/cdm.py new file mode 100644 index 0000000..3901e3c --- /dev/null +++ b/pymonalisa/pymonalisa/cdm.py @@ -0,0 +1,436 @@ +import re +import base64 +import uuid +from typing import Dict, List, Optional, Union +import wasmtime + +from pymonalisa.module import Module +from pymonalisa.license import License +from pymonalisa.types import Key, KeyType +from pymonalisa.exceptions import MonalisaSessionError, MonalisaLicenseError +from pymonalisa.utils import get_env_strings + + +class Cdm: + """MonaLisa Content Decryption Module""" + + def __init__(self, module: Module): + """ + Initialize CDM with module + + Args: + module: MonaLisa module instance + """ + self.module = module + self._sessions: Dict[str, 'Session'] = {} + + + @classmethod + def from_module(cls, module: Module) -> 'Cdm': + """ + Create CDM from module + + Args: + module: MonaLisa module instance + + Returns: + Cdm: CDM instance + """ + return cls(module) + + def open(self) -> str: + """ + Open new CDM session + + Returns: + str: Session ID + """ + session_id = str(uuid.uuid4()) + store = self.module.create_store() + session = Session(session_id, self.module, store) + session.initialize() + self._sessions[session_id] = session + return session_id + + def close(self, session_id: str): + """ + Close CDM session + + Args: + session_id: Session ID to close + """ + if session_id in self._sessions: + self._sessions[session_id].cleanup() + del self._sessions[session_id] + + def get_license_challenge(self, session_id: str, ticket: str): + return License(ticket) + + def parse_license(self, session_id: str, license: Union[License, str, bytes]): + """ + Parse license and extract keys directly + + Args: + session_id: Session ID + license: License data (MonaLisa license string/bytes) + """ + session = self._get_session(session_id) + + if not isinstance(license, License): + license = License(license) + + session.parse_license(license) + + def get_keys(self, session_id: str, key_type: Optional[KeyType] = KeyType.CONTENT) -> List[Key]: + """ + Get keys from session + + Args: + session_id: Session ID + key_type: Optional key type filter + + Returns: + List[Key]: List of keys + """ + session = self._get_session(session_id) + return session.get_keys(key_type) + + def _get_session(self, session_id: str) -> 'Session': + """Get session by ID""" + if session_id not in self._sessions: + raise MonalisaSessionError(f"Session not found: {session_id}") + return self._sessions[session_id] + + def __del__(self): + """Cleanup all sessions on destruction""" + for session_id in list(self._sessions.keys()): + self.close(session_id) + + +class Session: + """MonaLisa CDM session""" + + # Memory configuration constants + DYNAMIC_BASE = 6065008 + DYNAMICTOP_PTR = 821968 + LICENSE_KEY_OFFSET = 0x5C8C0C + LICENSE_KEY_LENGTH = 16 + + def __init__(self, session_id: str, module: Module, store: wasmtime.Store): + """Initialize session""" + self.session_id = session_id + self.module = module + self.store = store + self.instance = None + self.memory = None + self.exports = {} + self._ctx = None + self._keys: List[Key] = [] + self._initialized = False + + def initialize(self): + """Initialize WASM instance and context""" + if self._initialized: + return + + try: + # Create memory + memory_type = wasmtime.MemoryType(wasmtime.Limits(256, 256)) + self.memory = wasmtime.Memory(self.store, memory_type) + + # Set up dynamic memory pointer + self._write_i32(self.DYNAMICTOP_PTR, self.DYNAMIC_BASE) + + # Build imports + imports = self._build_imports() + + # Initialize WASM instance + self.instance = wasmtime.Instance(self.store, self.module.module, imports) + + # Get exports + self.exports = { + '___wasm_call_ctors': self.instance.exports(self.store)["s"], + '_monalisa_context_alloc': self.instance.exports(self.store)["D"], + 'monalisa_set_license': self.instance.exports(self.store)["F"], + '_monalisa_set_canvas_id': self.instance.exports(self.store)["t"], + '_monalisa_version_get': self.instance.exports(self.store)["A"], + 'monalisa_get_line_number': self.instance.exports(self.store)["v"], + 'stackAlloc': self.instance.exports(self.store)["N"], + 'stackSave': self.instance.exports(self.store)["L"], + 'stackRestore': self.instance.exports(self.store)["M"], + } + + # Initialize MonaLisa context + self.exports['___wasm_call_ctors'](self.store) + self._ctx = self.exports['_monalisa_context_alloc'](self.store) + self._initialized = True + + except Exception as e: + raise MonalisaSessionError(f"Failed to initialize session: {e}") + + def parse_license(self, license_: License): + """Parse license and extract keys directly""" + try: + # Use the base64 string format for WASM module + license_str = license_.b64 + + # Set license in WASM module + ret = self._ccall('monalisa_set_license', int, self._ctx, license_str, len(license_str), '0') + + if ret != 0: + raise MonalisaLicenseError(f"License validation failed with code: {ret}") + + # Extract license key from memory + key_hex = self._extract_license_key() + key_bytes = bytes.fromhex(key_hex) + + # Extract CID from license for KID generation + m = re.search(r'DCID-[A-Z0-9]+-[A-Z0-9]+-\d{8}-\d{6}-[A-Z0-9]+-\d{10}-[A-Z0-9]+', base64.b64decode(license_str).decode('ascii', errors='ignore')) + if m: + kid = uuid.uuid5(uuid.NAMESPACE_DNS, m.group()) + else: + kid = uuid.UUID(int=0) # default if fails + + # Create key object (assuming CONTENT key for now) + key = Key( + kid=kid.bytes, + key=key_bytes, + type=KeyType.CONTENT + ) + + self._keys.append(key) + + except Exception as e: + raise MonalisaLicenseError(f"Failed to parse license: {e}") + + def get_keys(self, key_type: Optional[KeyType] = None) -> List[Key]: + """Get keys from session""" + if key_type: + return [key for key in self._keys if key.type == key_type] + return self._keys.copy() + + def cleanup(self): + """Cleanup session resources""" + self._keys.clear() + self._ctx = None + self.instance = None + self.memory = None + self._initialized = False + + def _extract_license_key(self) -> str: + """Extract license key from memory""" + data = self.memory.data_ptr(self.store) + data_len = self.memory.data_len(self.store) + + if self.LICENSE_KEY_OFFSET + self.LICENSE_KEY_LENGTH > data_len: + raise MonalisaLicenseError("License key offset beyond memory bounds") + + # Read key bytes from memory + import ctypes + mem_ptr = ctypes.cast(data, ctypes.POINTER(ctypes.c_ubyte * data_len)) + key_bytes = bytes(mem_ptr.contents[self.LICENSE_KEY_OFFSET:self.LICENSE_KEY_OFFSET + self.LICENSE_KEY_LENGTH]) + return key_bytes.hex() + + def _ccall(self, func_name: str, return_type: type, *args): + """Call WASM function with argument conversion""" + stack = 0 + converted_args = [] + + for arg in args: + if isinstance(arg, str): + if stack == 0: + stack = self.exports['stackSave'](self.store) + max_length = (len(arg) << 2) + 1 + ptr = self.exports['stackAlloc'](self.store, max_length) + self._string_to_utf8(arg, ptr, max_length) + converted_args.append(ptr) + elif isinstance(arg, list): + if stack == 0: + stack = self.exports['stackSave'](self.store) + ptr = self.exports['stackAlloc'](self.store, len(arg)) + self._write_array_to_memory(arg, ptr) + converted_args.append(ptr) + else: + converted_args.append(arg) + + result = self.exports[func_name](self.store, *converted_args) + + if stack != 0: + self.exports['stackRestore'](self.store, stack) + + # Convert return value + if return_type == str: + return self._utf8_to_string(result) + elif return_type == bool: + return bool(result) + return result + + def _write_i32(self, addr: int, value: int): + """Write 32-bit integer to memory""" + data = self.memory.data_ptr(self.store) + import ctypes + mem_ptr = ctypes.cast(data, ctypes.POINTER(ctypes.c_int32)) + mem_ptr[addr >> 2] = value + + def _read_i32(self, addr: int) -> int: + """Read 32-bit integer from memory""" + data = self.memory.data_ptr(self.store) + import ctypes + mem_ptr = ctypes.cast(data, ctypes.POINTER(ctypes.c_int32)) + return mem_ptr[addr >> 2] + + def _string_to_utf8(self, data: str, ptr: int, max_length: int) -> int: + """Convert string to UTF-8 and write to memory""" + encoded = data.encode('utf-8') + write_length = min(len(encoded), max_length - 1) + + mem_data = self.memory.data_ptr(self.store) + import ctypes + mem_ptr = ctypes.cast(mem_data, ctypes.POINTER(ctypes.c_ubyte)) + + for i in range(write_length): + mem_ptr[ptr + i] = encoded[i] + mem_ptr[ptr + write_length] = 0 # null terminator + + return write_length + + def _write_array_to_memory(self, array: List, ptr: int): + """Write array to memory""" + mem_data = self.memory.data_ptr(self.store) + import ctypes + mem_ptr = ctypes.cast(mem_data, ctypes.POINTER(ctypes.c_ubyte)) + + for i, val in enumerate(array): + mem_ptr[ptr + i] = val + return ptr + + def _utf8_to_string(self, ptr: int) -> str: + """Convert UTF-8 from memory to string""" + if ptr == 0: + return '' + + mem_data = self.memory.data_ptr(self.store) + data_len = self.memory.data_len(self.store) + + if ptr >= data_len: + return '' + + import ctypes + mem_ptr = ctypes.cast(mem_data, ctypes.POINTER(ctypes.c_ubyte)) + + # Find null terminator + length = 0 + while ptr + length < data_len and mem_ptr[ptr + length] != 0: + length += 1 + + # Extract bytes and decode + data = bytes(mem_ptr[ptr:ptr + length]) + return data.decode('utf-8') + + def _write_ascii_to_memory(self, string: str, buffer: int, dont_add_null: int = 0): + """Write ASCII string to memory""" + mem_data = self.memory.data_ptr(self.store) + import ctypes + mem_ptr = ctypes.cast(mem_data, ctypes.POINTER(ctypes.c_ubyte)) + + encoded = string.encode('utf-8') + for i, byte_val in enumerate(encoded): + mem_ptr[buffer + i] = byte_val + + if dont_add_null == 0: + mem_ptr[buffer + len(encoded)] = 0 + + def _build_imports(self): + """Build import object with required external functions""" + + # System call stubs + def sys_fcntl64(param_0: int, param_1: int, param_2: int) -> int: + return 0 + def fd_write(param_0: int, param_1: int, param_2: int, param_3: int) -> int: + return 0 + def fd_close(param_0: int) -> int: + return 0 + def sys_ioctl(param_0: int, param_1: int, param_2: int) -> int: + return 0 + def sys_open(param_0: int, param_1: int, param_2: int) -> int: + return 0 + def sys_rmdir(param_0: int) -> int: + return 0 + def sys_unlink(param_0: int) -> int: + return 0 + def clock() -> int: + return 0 + def time(param_0: int) -> int: + return 0 + def emscripten_run_script(param_0: int): + pass + def fd_seek(param_0: int, param_1: int, param_2: int, param_3: int, param_4: int) -> int: + return 0 + def emscripten_resize_heap(param_0: int) -> int: + return 0 + def fd_read(param_0: int, param_1: int, param_2: int, param_3: int) -> int: + return 0 + def emscripten_run_script_string(param_0: int) -> int: + return 0 + def emscripten_run_script_int(param_0: int) -> int: + return 1 + + def emscripten_memcpy_big(dest: int, src: int, num: int) -> int: + mem_data = self.memory.data_ptr(self.store) + data_len = self.memory.data_len(self.store) + + if num is None: + num = data_len - 1 + + import ctypes + mem_ptr = ctypes.cast(mem_data, ctypes.POINTER(ctypes.c_ubyte)) + + # Copy memory + for i in range(num): + if dest + i < data_len and src + i < data_len: + mem_ptr[dest + i] = mem_ptr[src + i] + + return dest + + def environ_get(environ_ptr: int, environ_buf: int) -> int: + buf_size = 0 + strings = get_env_strings() + + for index, string in enumerate(strings): + ptr = environ_buf + buf_size + self._write_i32(environ_ptr + index * 4, ptr) + self._write_ascii_to_memory(string, ptr) + buf_size += len(string) + 1 + return 0 + + def environ_sizes_get(penviron_count: int, penviron_buf_size: int) -> int: + strings = get_env_strings() + self._write_i32(penviron_count, len(strings)) + buf_size = sum(len(string) + 1 for string in strings) + self._write_i32(penviron_buf_size, buf_size) + return 0 + + # Create function types and instances + imports = [ + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), sys_fcntl64), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), fd_write), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), fd_close), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), sys_ioctl), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), sys_open), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), sys_rmdir), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), sys_unlink), + wasmtime.Func(self.store, wasmtime.FuncType([], [wasmtime.ValType.i32()]), clock), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), time), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32()], []), emscripten_run_script), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), fd_seek), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), emscripten_memcpy_big), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), emscripten_resize_heap), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), environ_get), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), environ_sizes_get), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32(), wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), fd_read), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), emscripten_run_script_string), + wasmtime.Func(self.store, wasmtime.FuncType([wasmtime.ValType.i32()], [wasmtime.ValType.i32()]), emscripten_run_script_int), + self.memory, + ] + + return imports \ No newline at end of file diff --git a/pymonalisa/pymonalisa/exceptions.py b/pymonalisa/pymonalisa/exceptions.py new file mode 100644 index 0000000..a140cf5 --- /dev/null +++ b/pymonalisa/pymonalisa/exceptions.py @@ -0,0 +1,18 @@ +class MonalisaError(Exception): + """Base exception for all MonaLisa operations""" + pass + + +class MonalisaLicenseError(MonalisaError): + """Exception raised for license-related errors""" + pass + + +class MonalisaModuleError(MonalisaError): + """Exception raised for module/WASM module errors""" + pass + + +class MonalisaSessionError(MonalisaError): + """Exception raised for session management errors""" + pass \ No newline at end of file diff --git a/pymonalisa/pymonalisa/license.py b/pymonalisa/pymonalisa/license.py new file mode 100644 index 0000000..bad2cbf --- /dev/null +++ b/pymonalisa/pymonalisa/license.py @@ -0,0 +1,54 @@ +import base64 +from typing import Union + + +class License: + """MonaLisa license representation""" + + def __init__(self, data: Union[str, bytes]): + """ + Initialize License + + Args: + data: License data (base64 string or raw bytes) + """ + if isinstance(data, str): + try: + self.data = base64.b64decode(data) + self.data_b64 = data + except Exception: + # If not base64, treat as raw string + self.data = data.encode('utf-8') + self.data_b64 = base64.b64encode(self.data).decode('utf-8') + else: + self.data = data + self.data_b64 = base64.b64encode(data).decode('utf-8') + + @classmethod + def from_ticket(cls, ticket_data: Union[str, bytes]) -> 'License': + """ + Create License from TICKET data + + Args: + ticket_data: ticket data (base64 string) + + Returns: + License: License instance + """ + return cls(ticket_data) + + @property + def raw(self) -> bytes: + """Get raw license data""" + return self.data + + @property + def b64(self) -> str: + """Get base64 encoded license data""" + return self.data_b64 + + def __str__(self) -> str: + return self.data_b64 + + def __repr__(self) -> str: + return f"License(data='{self.data_b64}')" \ No newline at end of file diff --git a/pymonalisa/pymonalisa/main.py b/pymonalisa/pymonalisa/main.py new file mode 100644 index 0000000..3a80c02 --- /dev/null +++ b/pymonalisa/pymonalisa/main.py @@ -0,0 +1,170 @@ +import logging +import click +import cloup +from loggpy import logging, Logger +from datetime import datetime +from pathlib import Path +from typing import Optional + +from pymonalisa import __version__ +from pymonalisa.cdm import Cdm +from pymonalisa.module import Module +from pymonalisa.license import License +from pymonalisa.types import KeyType +from pymonalisa.exceptions import ( + MonalisaError, + MonalisaLicenseError, + MonalisaModuleError, + MonalisaSessionError +) + + +@cloup.group( + name='pymonalisa', + invoke_without_command=True, + context_settings=cloup.Context.settings( + help_option_names=["-h", "--help"], + max_content_width=116, + align_option_groups=False, + align_sections=True, + formatter_settings=cloup.HelpFormatter.settings( + indent_increment=3, + col1_max_width=50, + col_spacing=3, + theme=cloup.HelpTheme( + invoked_command=cloup.Style(fg="cyan"), + heading=cloup.Style(fg="bright_white", bold=True), + col1=cloup.Style(fg="bright_green"), + ), + ), +), + help=click.style( + text="A Python library to decrypt IQIYI DRM License Ticket", + fg='green', + dim=True, + overline=True, + strikethrough=True, + blink=True, + bold=True, + ), + epilog=click.style( + text=""" + \b + Made for fun only by ReiDoBrega. + + \b + Thanks to xhlove and duck + """, + fg='blue', + bold=True, + italic=True, + ) +) + + +@cloup.option("-v", "--version", is_flag=True, default=False, help="Print version information.") +def main(version: bool) -> None: + """Python MonaLisa CDM implementation""" + + Logger.mount(level=logging.INFO) + log = Logger('pymonalisa') + + current_year = datetime.now().year + copyright_years = f"2025-{current_year}" if current_year > 2025 else "2025" + + log.info("pymonalisa version %s Copyright (c) %s pymonalisa Team", __version__, copyright_years) + log.info("MonaLisa Content Decryption Module for Python") + log.info("Run 'pymonalisa --help' for help") + + if version: + return + + +@main.command(name="license") +@cloup.argument("license_data", type=str) +@cloup.argument("device_path", type=cloup.Path(exists=True, path_type=Path)) +@cloup.option_group( + "Key filtering options", + cloup.option( + "-t", "--key-type", + type=cloup.Choice(["CONTENT", "FULL"], case_sensitive=False), + default="CONTENT", + help="Filter keys by type" + ), +) +def license_( + license_data: str, + device_path: Path, + key_type: Optional[str], +) -> None: + """ + Process a MonaLisa encoded license and extract decryption keys. + + LICENSE_DATA: Base64 encoded MonaLisa license string + DEVICE_PATH: Path to MonaLisa device file (.mld) + + Example: + pymonalisa license "AIUACgMAAAAAAAAAAAQChgACATADhwAnAgAg3UBbUdVCWXAjkgoUgmICmHvomvZai0jGglWe+oaQC+M..." device.mld + """ + log = logging.getLogger("license") + + # Validate inputs + if not device_path.is_file(): + log.error(f"Module file not found: {device_path}") + return + + try: + log.info(f"Loading module: {device_path}") + module = Module.load(device_path) + log.info(f"Loaded module successfully") + + log.info("Initializing CDM...") + cdm = Cdm.from_module(module) + log.info("CDM initialized successfully") + + log.info("Opening CDM session...") + session_id = cdm.open() + log.info(f"Session opened: {session_id}") + + log.info("Processing license data...") + license_obj = License(license_data) + + log.info("Parsing license and extracting keys...") + cdm.parse_license(session_id, license_obj) + log.info("License parsed successfully") + + keys = cdm.get_keys(session_id, KeyType.CONTENT) # CONTENT only for now + + if not keys: + log.warning("No keys found in license") + cdm.close(session_id) + return + + log.info(f"Found {len(keys)} keys:") + for key in keys: # One only always, dont sure about others verions up LicenseVersion 3 + # NOTE: we can handle here but the key_type always will be CONTENT, to decrypt the IQIYI bbts we need the KEY only, not the KID + if key_type == "CONTENT": + log.info(f"{key.key.hex()}") + else: + log.info(f"{key.kid.hex()}:{key.key.hex()}") + + cdm.close(session_id) + log.info("Session closed successfully") + + except MonalisaModuleError as e: + log.error(f"Module error: {e}") + except MonalisaLicenseError as e: + log.error(f"License error: {e}") + except MonalisaSessionError as e: + log.error(f"Session error: {e}") + except MonalisaError as e: + log.error(f"MonaLisa error: {e}") + except Exception as e: + log.error(f"Unexpected error: {e}") + if log.isEnabledFor(logging.DEBUG): + import traceback + log.debug(traceback.format_exc()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pymonalisa/pymonalisa/module.py b/pymonalisa/pymonalisa/module.py new file mode 100644 index 0000000..a1a09e9 --- /dev/null +++ b/pymonalisa/pymonalisa/module.py @@ -0,0 +1,116 @@ +import json +from pathlib import Path +from typing import Union, Dict, Any +import wasmtime + +from pymonalisa.exceptions import MonalisaModuleError + + +class Module: + """MonaLisa module representation with WASM module""" + + def __init__(self, wasm_path: Union[str, Path], metadata: Dict[str, Any] = None): + """ + Initialize Device with WASM module + + Args: + wasm_path: Path to MonaLisa WASM file (.wat or .wasm) + metadata: Optional module metadata + """ + self.wasm_path = Path(wasm_path) + self.metadata = metadata or {} + self._engine = None + self._module = None + self._validate_and_load() + + def _validate_and_load(self): + """Validate and load WASM module""" + if not self.wasm_path.exists(): + raise MonalisaModuleError(f"WASM file not found: {self.wasm_path}") + + try: + # Initialize engine + self._engine = wasmtime.Engine() + + # Load WASM file + if self.wasm_path.suffix.lower() == '.wat': + self._module = wasmtime.Module.from_file(self._engine, str(self.wasm_path)) + else: + wasm_bytes = self.wasm_path.read_bytes() + self._module = wasmtime.Module(self._engine, wasm_bytes) + + except Exception as e: + raise MonalisaModuleError(f"Failed to load WASM module: {e}") + + @classmethod + def load(cls, module_path: Union[str, Path]) -> 'Module': + """ + Load module from .mld (MonaLisa Device) file + + Args: + module_path: Path to .mld file containing module info + + Returns: + Module: Loaded module instance + """ + module_path = Path(module_path) + + if not module_path.exists(): + raise MonalisaModuleError(f"Device file not found: {module_path}") + + try: + with open(module_path, 'r') as f: + device_data = json.load(f) + + wasm_path = device_data.get('wasm_path') + if not wasm_path: + raise MonalisaModuleError("Device file missing wasm_path") + + # Resolve relative path + if not Path(wasm_path).is_absolute(): + wasm_path = module_path.parent / wasm_path + + metadata = device_data.get('metadata', {}) + return cls(wasm_path, metadata) + + except json.JSONDecodeError: + raise MonalisaModuleError(f"Invalid module file format: {module_path}") + except Exception as e: + raise MonalisaModuleError(f"Failed to load module: {e}") + + def save(self, module_path: Union[str, Path]): + """ + Save module to .mld file + + Args: + module_path: Path where to save the device file + """ + module_path = Path(module_path) + + device_data = { + 'wasm_path': str(self.wasm_path), + 'metadata': self.metadata + } + + try: + with open(module_path, 'w') as f: + json.dump(device_data, f, indent=2) + except Exception as e: + raise MonalisaModuleError(f"Failed to save device: {e}") + + def create_store(self) -> wasmtime.Store: + """Create fresh store instance for WASM execution""" + return wasmtime.Store(self._engine) + + @property + def engine(self) -> wasmtime.Engine: + """Get WASM engine""" + return self._engine + + @property + def module(self) -> wasmtime.Module: + """Get WASM module""" + return self._module + + def __repr__(self) -> str: + return f"Device(wasm_path='{self.wasm_path}')" \ No newline at end of file diff --git a/pymonalisa/pymonalisa/types.py b/pymonalisa/pymonalisa/types.py new file mode 100644 index 0000000..5069adc --- /dev/null +++ b/pymonalisa/pymonalisa/types.py @@ -0,0 +1,23 @@ +from enum import Enum +from dataclasses import dataclass +from typing import Optional + + +class KeyType(Enum): + """Key types for MonaLisa keys""" + CONTENT = "CONTENT" + SIGNING = "SIGNING" + OTT = "OTT" + OPERATOR_SESSION = "OPERATOR_SESSION" + + +@dataclass +class Key: + """Represents a MonaLisa key""" + kid: bytes + key: bytes + type: KeyType + permissions: Optional[list] = None + + def __str__(self) -> str: + return f"[{self.type.value}] {self.kid.hex()}:{self.key.hex()}" \ No newline at end of file diff --git a/pymonalisa/pymonalisa/utils.py b/pymonalisa/pymonalisa/utils.py new file mode 100644 index 0000000..c78a749 --- /dev/null +++ b/pymonalisa/pymonalisa/utils.py @@ -0,0 +1,22 @@ +from typing import List + +def get_env_strings() -> List[str]: + return [ + 'USER=web_user', + 'LOGNAME=web_user', + 'PATH=/', + 'PWD=/', + 'HOME=/home/web_user', + 'LANG=zh_CN.UTF-8', + '_=./this.program' + ] + + +def bytes_to_hex(data: bytes) -> str: + """Convert bytes to uppercase hex string""" + return data.hex().upper() + + +def hex_to_bytes(hex_str: str) -> bytes: + """Convert hex string to bytes""" + return bytes.fromhex(hex_str) diff --git a/pymonalisa/pyproject.toml b/pymonalisa/pyproject.toml new file mode 100644 index 0000000..900b1b6 --- /dev/null +++ b/pymonalisa/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "pymonalisa" +version = "0.1.2" +description = "pymonalisa - A Python library to decrypt IQIYI DRM License Ticket" +license = "CC BY-NC-ND 4.0" +authors = ["ReiDoBrega", "duck", "xhlove"] +readme = "README.md" +repository = "https://github.com/ReiDoBrega/pymonalisa" +keywords = ["python", "drm", "monalisa", "iqcom", "iqiyi", "wasm"] +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/pymonalisa/pymonalisa/issues" + +[tool.poetry.dependencies] +python = ">=3.9,<4.0" +click = "^8.1.7" +cloup = "^3.0.7" +loggpy = "^0.1.0" +wasmtime = "^36.0.0" + +[tool.poetry.scripts] +pymonalisa = "pymonalisa.main:main" \ No newline at end of file diff --git a/pymonalisa/setup.py b/pymonalisa/setup.py new file mode 100644 index 0000000..0ff0244 --- /dev/null +++ b/pymonalisa/setup.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from setuptools import setup, find_packages + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup( + name="pymonalisa", + version="0.1.2", + description="pymonalisa - A Python library to decrypt IQIYI DRM License Ticket", + long_description=long_description, + long_description_content_type="text/markdown", + author="ReiDoBrega", + author_email="", + url="https://github.com/ReiDoBrega/pymonalisa", + license="CC BY-NC-ND 4.0", + packages=find_packages(), + python_requires=">=3.9,<4.0", + install_requires=[ + "click>=8.1.7", + "cloup>=3.0.7", + "loggpy>=0.1.0", + "wasmtime>=36.0.0", + ], + entry_points={ + "console_scripts": [ + "pymonalisa=pymonalisa.main:main", + ], + }, + keywords=[ + "python", + "drm", + "monalisa", + "iqcom", + "iqiyi", + "wasm", + ], + 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", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + include_package_data=True, +)