commit 97b8a79be81fd83c1c87363bf1efa6c47026eacb
Author: larley <121249322+DevLARLEY@users.noreply.github.com>
Date: Wed Aug 20 19:37:46 2025 +0200
Initial Commit
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1efa2ae
--- /dev/null
+++ b/README.md
@@ -0,0 +1,142 @@
+# pywidevine
+
+## Features
+
+- 🚀 Seamless Installation via [pip](#installation)
+- 🛡️ Robust Security with message signature verification
+- 🙈 Privacy Mode with Service Certificates
+- 🌐 Servable CDM API Server and Client with Authentication
+- 📦 Custom provision serialization format (WVD v2)
+- 🧰 Create, parse, or convert PSSH headers with ease
+- 🗃️ User-friendly YAML configuration
+- ❤️ Forever FOSS!
+
+## Installation
+
+```shell
+$ pip install pywidevine
+```
+
+> **Note**
+If pip gives you a warning about a path not being in your PATH environment variable then promptly add that path then
+close all open command prompt/terminal windows, or `pywidevine` CLI won't work as it will not be found.
+
+Voilà 🎉 — You now have the `pywidevine` package installed!
+You can now import pywidevine in scripts ([see below](#usage)).
+A command-line interface is also available, try `pywidevine --help`.
+
+## Usage
+
+The following is a minimal example of using pywidevine in a script to get a License for Bitmovin's
+Art of Motion Demo.
+
+```py
+from pywidevine.cdm import Cdm
+from pywidevine.device import Device
+from pywidevine.pssh import PSSH
+
+import requests
+
+# prepare pssh
+pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
+ "7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
+
+# load device
+device = Device.load("C:/Path/To/A/Provision.wvd")
+
+# load cdm
+cdm = Cdm.from_device(device)
+
+# open cdm session
+session_id = cdm.open()
+
+# get license challenge
+challenge = cdm.get_license_challenge(session_id, pssh)
+
+# send license challenge (assuming a generic license server SDK with no API front)
+licence = requests.post("https://...", data=challenge)
+licence.raise_for_status()
+
+# parse license challenge
+cdm.parse_license(session_id, licence.content)
+
+# print keys
+for key in cdm.get_keys(session_id):
+ print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
+
+# close session, disposes of session data
+cdm.close(session_id)
+```
+
+> **Note**
+> There are various features not shown in this specific example like:
+>
+> - Privacy Mode
+> - Setting Service Certificates
+> - Remote CDMs and Serving
+> - Choosing a License Type to request
+> - Creating WVD files
+> - and much more!
+>
+> Take a look at the methods available in the [Cdm class](/pywidevine/cdm.py) and their doc-strings for
+> further information. For more examples see the [CLI functions](/pywidevine/main.py) which uses a lot
+> of previously mentioned features.
+
+## Disclaimer
+
+1. This project requires a valid Google-provisioned Private Key and Client Identification blob which are not
+ provided by this project.
+2. Public test provisions are available and provided by Google to use for testing projects such as this one.
+3. License Servers have the ability to block requests from any provision, and are likely already blocking test
+ provisions on production endpoints.
+4. This project does not condone piracy or any action against the terms of the DRM systems.
+5. All efforts in this project have been the result of Reverse-Engineering, Publicly available research, and Trial
+ & Error.
+
+## Key and Output Security
+
+*Licenses, Content Keys, and Decrypted Data is not secure in this CDM implementation.*
+
+The Content Decryption Module is meant to do all downloading, decrypting, and decoding of content, not just license
+acquisition. This Python implementation only does License Acquisition within the CDM.
+
+The section of which a 'Decrypt Frame' call is made would be more of a 'Decrypt File' in this implementation. Just
+returning the original file in plain text defeats the point of the DRM. Even if 'Decrypt File' was somehow secure, the
+Content Keys used to decrypt the files are already exposed to the caller anyway, allowing them to manually decrypt.
+
+An attack on a 'Decrypt Frame' system would be analogous to doing an HDMI capture or similar attack. This is because it
+would require re-encoding the video by splicing each individual frame with the right frame-rate, syncing to audio, and
+more.
+
+While a 'Decrypt Video' system would be analogous to downloading a Video and passing it through a script. Not much of
+an attack if at all. The only protection against a system like this would be monitoring the provision and acquisitions
+of licenses and prevent them. This can be done by revoking the device provision, or the user or their authorization to
+the service.
+
+There isn't any immediate way to secure either Key or Decrypted information within a Python environment that is not
+Hardware backed. Even if obfuscation or some other form of Security by Obscurity was used, this is a Software-based
+Content Protection Module (in Python no less) with no hardware backed security. It would be incredibly trivial to break
+any sort of protection against retrieving the original video data.
+
+Though, it's not impossible. Google's Chrome Browser CDM is a simple library extension file programmed in C++ that has
+been improving its security using math and obscurity for years. It's getting harder and harder to break with its latest
+versions only being beaten by Brute-force style methods. However, they have a huge team of very skilled workers, and
+making a CDM in C++ has immediate security benefits and a lot of methods to obscure and obfuscate the code.
+
+## Contributors
+
+
+
+
+
+## Licensing
+
+This software is licensed under the terms of [GNU General Public License, Version 3.0](LICENSE).
+You can find a copy of the license in the LICENSE file in the root folder.
+
+- Widevine Icon © Google.
+- Props to the awesome community for their shared research and insight into the Widevine Protocol and Key Derivation.
+
+* * *
+
+© rlaphoenix 2022-2023
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..7698ed2
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,48 @@
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "pywidevine"
+version = "1.8.0"
+description = "Widevine CDM (Content Decryption Module) implementation in Python."
+license = "GPL-3.0-only"
+authors = ["rlaphoenix ", "DevLARLEY"]
+readme = "README.md"
+repository = "https://git.gay/ready-dl/pywidevine/pywidevine"
+keywords = ["python", "drm", "widevine", "google"]
+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 = "CHANGELOG.md", format = "sdist" },
+ { path = "README.md", format = "sdist" },
+ { path = "LICENSE", format = "sdist" },
+]
+
+[tool.poetry.urls]
+"Issues" = "https://git.gay/ready-dl/pywidevine/pywidevine/issues"
+
+[tool.poetry.dependencies]
+python = ">=3.8"
+protobuf = "^4.25.1"
+pymp4 = "^1.4.0"
+pycryptodome = "^3.19.0"
+click = "^8.1.7"
+requests = "^2.31.0"
+Unidecode = "^1.3.7"
+PyYAML = "^6.0.1"
+aiohttp = {version = "^3.9.1", optional = true}
+
+[tool.poetry.extras]
+serve = ["aiohttp"]
+
+[tool.poetry.scripts]
+pywidevine = "pywidevine.main:main"
diff --git a/pywidevine/__init__.py b/pywidevine/__init__.py
new file mode 100644
index 0000000..659ea25
--- /dev/null
+++ b/pywidevine/__init__.py
@@ -0,0 +1,8 @@
+from .cdm import *
+from .device import *
+from .key import *
+from .pssh import *
+from .remotecdm import *
+from .session import *
+
+__version__ = "1.8.0"
diff --git a/pywidevine/cdm.py b/pywidevine/cdm.py
new file mode 100644
index 0000000..256cedf
--- /dev/null
+++ b/pywidevine/cdm.py
@@ -0,0 +1,658 @@
+from __future__ import annotations
+
+import base64
+import binascii
+import random
+import subprocess
+import sys
+import time
+from pathlib import Path
+from typing import Optional, Union
+from uuid import UUID
+
+from Crypto.Cipher import AES, PKCS1_OAEP
+from Crypto.Hash import CMAC, HMAC, SHA1, SHA256
+from Crypto.PublicKey import RSA
+from Crypto.Random import get_random_bytes
+from Crypto.Signature import pss
+from Crypto.Util import Padding
+from google.protobuf.message import DecodeError
+
+from pywidevine.device import Device, DeviceTypes
+from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
+ InvalidSession, NoKeysLoaded, SignatureMismatch, TooManySessions)
+from pywidevine.key import Key
+from pywidevine.license_protocol_pb2 import (ClientIdentification, DrmCertificate, EncryptedClientIdentification,
+ License, LicenseRequest, LicenseType, SignedDrmCertificate,
+ SignedMessage)
+from pywidevine.pssh import PSSH
+from pywidevine.session import Session
+from pywidevine.utils import get_binary_path
+
+
+class Cdm:
+ uuid = UUID(bytes=b"\xed\xef\x8b\xa9\x79\xd6\x4a\xce\xa3\xc8\x27\xdc\xd5\x1d\x21\xed")
+ urn = f"urn:uuid:{uuid}"
+ key_format = urn
+ service_certificate_challenge = b"\x08\x04"
+ common_privacy_cert = (
+ # Used by Google's production license server (license.google.com)
+ # Not publicly accessible directly, but a lot of services have their own gateways to it
+ "CAUSxwUKwQIIAxIQFwW5F8wSBIaLBjM6L3cqjBiCtIKSBSKOAjCCAQoCggEBAJntWzsyfateJO/DtiqVtZhSCtW8yzdQPgZFuBTYdrjfQFEE"
+ "Qa2M462xG7iMTnJaXkqeB5UpHVhYQCOn4a8OOKkSeTkwCGELbxWMh4x+Ib/7/up34QGeHleB6KRfRiY9FOYOgFioYHrc4E+shFexN6jWfM3r"
+ "M3BdmDoh+07svUoQykdJDKR+ql1DghjduvHK3jOS8T1v+2RC/THhv0CwxgTRxLpMlSCkv5fuvWCSmvzu9Vu69WTi0Ods18Vcc6CCuZYSC4NZ"
+ "7c4kcHCCaA1vZ8bYLErF8xNEkKdO7DevSy8BDFnoKEPiWC8La59dsPxebt9k+9MItHEbzxJQAZyfWgkCAwEAAToUbGljZW5zZS53aWRldmlu"
+ "ZS5jb20SgAOuNHMUtag1KX8nE4j7e7jLUnfSSYI83dHaMLkzOVEes8y96gS5RLknwSE0bv296snUE5F+bsF2oQQ4RgpQO8GVK5uk5M4PxL/C"
+ "CpgIqq9L/NGcHc/N9XTMrCjRtBBBbPneiAQwHL2zNMr80NQJeEI6ZC5UYT3wr8+WykqSSdhV5Cs6cD7xdn9qm9Nta/gr52u/DLpP3lnSq8x2"
+ "/rZCR7hcQx+8pSJmthn8NpeVQ/ypy727+voOGlXnVaPHvOZV+WRvWCq5z3CqCLl5+Gf2Ogsrf9s2LFvE7NVV2FvKqcWTw4PIV9Sdqrd+QLeF"
+ "Hd/SSZiAjjWyWOddeOrAyhb3BHMEwg2T7eTo/xxvF+YkPj89qPwXCYcOxF+6gjomPwzvofcJOxkJkoMmMzcFBDopvab5tDQsyN9UPLGhGC98"
+ "X/8z8QSQ+spbJTYLdgFenFoGq47gLwDS6NWYYQSqzE3Udf2W7pzk4ybyG4PHBYV3s4cyzdq8amvtE/sNSdOKReuHpfQ=")
+ staging_privacy_cert = (
+ # Used by Google's staging license server (staging.google.com)
+ # This can be publicly accessed without authentication using https://cwip-shaka-proxy.appspot.com/no_auth
+ "CAUSxQUKvwIIAxIQKHA0VMAI9jYYredEPbbEyBiL5/mQBSKOAjCCAQoCggEBALUhErjQXQI/zF2V4sJRwcZJtBd82NK+7zVbsGdD3mYePSq8"
+ "MYK3mUbVX9wI3+lUB4FemmJ0syKix/XgZ7tfCsB6idRa6pSyUW8HW2bvgR0NJuG5priU8rmFeWKqFxxPZmMNPkxgJxiJf14e+baq9a1Nuip+"
+ "FBdt8TSh0xhbWiGKwFpMQfCB7/+Ao6BAxQsJu8dA7tzY8U1nWpGYD5LKfdxkagatrVEB90oOSYzAHwBTK6wheFC9kF6QkjZWt9/v70JIZ2fz"
+ "PvYoPU9CVKtyWJOQvuVYCPHWaAgNRdiTwryi901goMDQoJk87wFgRwMzTDY4E5SGvJ2vJP1noH+a2UMCAwEAAToSc3RhZ2luZy5nb29nbGUu"
+ "Y29tEoADmD4wNSZ19AunFfwkm9rl1KxySaJmZSHkNlVzlSlyH/iA4KrvxeJ7yYDa6tq/P8OG0ISgLIJTeEjMdT/0l7ARp9qXeIoA4qprhM19"
+ "ccB6SOv2FgLMpaPzIDCnKVww2pFbkdwYubyVk7jei7UPDe3BKTi46eA5zd4Y+oLoG7AyYw/pVdhaVmzhVDAL9tTBvRJpZjVrKH1lexjOY9Dv"
+ "1F/FJp6X6rEctWPlVkOyb/SfEJwhAa/K81uDLyiPDZ1Flg4lnoX7XSTb0s+Cdkxd2b9yfvvpyGH4aTIfat4YkF9Nkvmm2mU224R1hx0WjocL"
+ "sjA89wxul4TJPS3oRa2CYr5+DU4uSgdZzvgtEJ0lksckKfjAF0K64rPeytvDPD5fS69eFuy3Tq26/LfGcF96njtvOUA4P5xRFtICogySKe6W"
+ "nCUZcYMDtQ0BMMM1LgawFNg4VA+KDCJ8ABHg9bOOTimO0sswHrRWSWX1XF15dXolCk65yEqz5lOfa2/fVomeopkU")
+ root_signed_cert = SignedDrmCertificate()
+ root_signed_cert.ParseFromString(base64.b64decode(
+ "CpwDCAASAQAY3ZSIiwUijgMwggGKAoIBgQC0/jnDZZAD2zwRlwnoaM3yw16b8udNI7EQ24dl39z7nzWgVwNTTPZtNX2meNuzNtI/nECplSZy"
+ "f7i+Zt/FIZh4FRZoXS9GDkPLioQ5q/uwNYAivjQji6tTW3LsS7VIaVM+R1/9Cf2ndhOPD5LWTN+udqm62SIQqZ1xRdbX4RklhZxTmpfrhNfM"
+ "qIiCIHAmIP1+QFAn4iWTb7w+cqD6wb0ptE2CXMG0y5xyfrDpihc+GWP8/YJIK7eyM7l97Eu6iR8nuJuISISqGJIOZfXIbBH/azbkdDTKjDOx"
+ "+biOtOYS4AKYeVJeRTP/Edzrw1O6fGAaET0A+9K3qjD6T15Id1sX3HXvb9IZbdy+f7B4j9yCYEy/5CkGXmmMOROtFCXtGbLynwGCDVZEiMg1"
+ "7B8RsyTgWQ035Ec86kt/lzEcgXyUikx9aBWE/6UI/Rjn5yvkRycSEbgj7FiTPKwS0ohtQT3F/hzcufjUUT4H5QNvpxLoEve1zqaWVT94tGSC"
+ "UNIzX5ECAwEAARKAA1jx1k0ECXvf1+9dOwI5F/oUNnVKOGeFVxKnFO41FtU9v0KG9mkAds2T9Hyy355EzUzUrgkYU0Qy7OBhG+XaE9NVxd0a"
+ "y5AeflvG6Q8in76FAv6QMcxrA4S9IsRV+vXyCM1lQVjofSnaBFiC9TdpvPNaV4QXezKHcLKwdpyywxXRESYqI3WZPrl3IjINvBoZwdVlkHZV"
+ "dA8OaU1fTY8Zr9/WFjGUqJJfT7x6Mfiujq0zt+kw0IwKimyDNfiKgbL+HIisKmbF/73mF9BiC9yKRfewPlrIHkokL2yl4xyIFIPVxe9enz2F"
+ "RXPia1BSV0z7kmxmdYrWDRuu8+yvUSIDXQouY5OcCwEgqKmELhfKrnPsIht5rvagcizfB0fbiIYwFHghESKIrNdUdPnzJsKlVshWTwApHQh7"
+ "evuVicPumFSePGuUBRMS9nG5qxPDDJtGCHs9Mmpoyh6ckGLF7RC5HxclzpC5bc3ERvWjYhN0AqdipPpV2d7PouaAdFUGSdUCDA=="
+ ))
+ root_cert = DrmCertificate()
+ root_cert.ParseFromString(root_signed_cert.drm_certificate)
+
+ MAX_NUM_OF_SESSIONS = 16
+
+ def __init__(
+ self,
+ device_type: Union[DeviceTypes, str],
+ system_id: int,
+ security_level: int,
+ client_id: ClientIdentification,
+ rsa_key: RSA.RsaKey
+ ):
+ """Initialize a Widevine Content Decryption Module (CDM)."""
+ if not device_type:
+ raise ValueError("Device Type must be provided")
+ if isinstance(device_type, str):
+ device_type = DeviceTypes[device_type]
+ if not isinstance(device_type, DeviceTypes):
+ raise TypeError(f"Expected device_type to be a {DeviceTypes!r} not {device_type!r}")
+
+ if not system_id:
+ raise ValueError("System ID must be provided")
+ if not isinstance(system_id, int):
+ raise TypeError(f"Expected system_id to be a {int} not {system_id!r}")
+
+ 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 client_id:
+ raise ValueError("Client ID must be provided")
+ if not isinstance(client_id, ClientIdentification):
+ raise TypeError(f"Expected client_id to be a {ClientIdentification} not {client_id!r}")
+
+ if not rsa_key:
+ raise ValueError("RSA Key must be provided")
+ if not isinstance(rsa_key, RSA.RsaKey):
+ raise TypeError(f"Expected rsa_key to be a {RSA.RsaKey} not {rsa_key!r}")
+
+ self.device_type = device_type
+ self.system_id = system_id
+ self.security_level = security_level
+ self.__client_id = client_id
+
+ self.__signer = pss.new(rsa_key)
+ self.__decrypter = PKCS1_OAEP.new(rsa_key)
+
+ self.__sessions: dict[bytes, Session] = {}
+
+ @classmethod
+ def from_device(cls, device: Device) -> Cdm:
+ """Initialize a Widevine CDM from a Widevine Device (.wvd) file."""
+ return cls(
+ device_type=device.type,
+ system_id=device.system_id,
+ security_level=device.security_level,
+ client_id=device.client_id,
+ rsa_key=device.private_key
+ )
+
+ def open(self) -> bytes:
+ """
+ Open a Widevine Content Decryption Module (CDM) session.
+
+ Raises:
+ TooManySessions: If the session cannot be opened as limit has been reached.
+ """
+ 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 Widevine Content Decryption Module (CDM) session.
+
+ Parameters:
+ session_id: Session identifier.
+
+ Raises:
+ InvalidSession: If the Session identifier is invalid.
+ """
+ session = self.__sessions.get(session_id)
+ if not session:
+ raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
+ del self.__sessions[session_id]
+
+ def set_service_certificate(self, session_id: bytes, certificate: Optional[Union[bytes, str]]) -> Optional[str]:
+ """
+ Set a Service Privacy Certificate for Privacy Mode. (optional but recommended)
+
+ The Service Certificate is used to encrypt Client IDs in Licenses. This is also
+ known as Privacy Mode and may be required for some services or for some devices.
+ Chrome CDM requires it as of the enforcement of VMP (Verified Media Path).
+
+ We reject direct DrmCertificates as they do not have signature verification and
+ cannot be verified. You must provide a SignedDrmCertificate or a SignedMessage
+ containing a SignedDrmCertificate.
+
+ Parameters:
+ session_id: Session identifier.
+ certificate: SignedDrmCertificate (or SignedMessage containing one) in Base64
+ or Bytes form obtained from the Service. Some services have their own,
+ but most use the common privacy cert, (common_privacy_cert). If None, it
+ will remove the current certificate.
+
+ Raises:
+ InvalidSession: If the Session identifier is invalid.
+ DecodeError: If the certificate could not be parsed as a SignedDrmCertificate
+ nor a SignedMessage containing a SignedDrmCertificate.
+ SignatureMismatch: If the Signature of the SignedDrmCertificate does not
+ match the underlying DrmCertificate.
+
+ Returns the Service Provider ID of the verified DrmCertificate if successful.
+ If certificate is None, it will return the now-unset certificate's Provider ID,
+ or None if no certificate was set yet.
+ """
+ session = self.__sessions.get(session_id)
+ if not session:
+ raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
+
+ if certificate is None:
+ if session.service_certificate:
+ drm_certificate = DrmCertificate()
+ drm_certificate.ParseFromString(session.service_certificate.drm_certificate)
+ provider_id = drm_certificate.provider_id
+ else:
+ provider_id = None
+ session.service_certificate = None
+ return provider_id
+
+ if isinstance(certificate, str):
+ try:
+ certificate = base64.b64decode(certificate) # assuming base64
+ except binascii.Error:
+ raise DecodeError("Could not decode certificate string as Base64, expected bytes.")
+ elif not isinstance(certificate, bytes):
+ raise DecodeError(f"Expecting Certificate to be bytes, not {certificate!r}")
+
+ signed_message = SignedMessage()
+ signed_drm_certificate = SignedDrmCertificate()
+ drm_certificate = DrmCertificate()
+
+ try:
+ signed_message.ParseFromString(certificate)
+ if all(
+ # See https://github.com/devine-dl/pywidevine/issues/41
+ bytes(chunk) == signed_message.SerializeToString()
+ for chunk in zip(*[iter(certificate)] * len(signed_message.SerializeToString()))
+ ):
+ signed_drm_certificate.ParseFromString(signed_message.msg)
+ else:
+ signed_drm_certificate.ParseFromString(certificate)
+ if signed_drm_certificate.SerializeToString() != certificate:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ # could be a direct unsigned DrmCertificate, but reject those anyway
+ raise DecodeError(f"Could not parse certificate as a SignedDrmCertificate, {e}")
+
+ try:
+ pss. \
+ new(RSA.import_key(self.root_cert.public_key)). \
+ verify(
+ msg_hash=SHA1.new(signed_drm_certificate.drm_certificate),
+ signature=signed_drm_certificate.signature
+ )
+ except (ValueError, TypeError):
+ raise SignatureMismatch("Signature Mismatch on SignedDrmCertificate, rejecting certificate")
+
+ try:
+ drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate)
+ if drm_certificate.SerializeToString() != signed_drm_certificate.drm_certificate:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ raise DecodeError(f"Could not parse signed certificate's message as a DrmCertificate, {e}")
+
+ # must be stored as a SignedDrmCertificate as the signature needs to be kept for RemoteCdm
+ # if we store as DrmCertificate (no signature) then RemoteCdm cannot verify the Certificate
+ session.service_certificate = signed_drm_certificate
+ return drm_certificate.provider_id
+
+ def get_service_certificate(self, session_id: bytes) -> Optional[SignedDrmCertificate]:
+ """
+ Get the currently set Service Privacy Certificate of the Session.
+
+ Parameters:
+ session_id: Session identifier.
+
+ Raises:
+ InvalidSession: If the Session identifier is invalid.
+
+ Returns the Service Certificate if one is set, otherwise None.
+ """
+ session = self.__sessions.get(session_id)
+ if not session:
+ raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
+
+ return session.service_certificate
+
+ def get_license_challenge(
+ self,
+ session_id: bytes,
+ pssh: PSSH,
+ license_type: str = "STREAMING",
+ privacy_mode: bool = True
+ ) -> bytes:
+ """
+ Get a License Request (Challenge) to send to a License Server.
+
+ Parameters:
+ session_id: Session identifier.
+ pssh: PSSH Object to get the init data from.
+ license_type: Type of License you wish to exchange, often `STREAMING`.
+ - "STREAMING": Normal one-time-use license.
+ - "OFFLINE": Offline-use licence, usually for Downloaded content.
+ - "AUTOMATIC": License type decision is left to provider.
+ privacy_mode: Encrypt the Client ID using the Privacy Certificate. If the
+ privacy certificate is not set yet, this does nothing.
+
+ Raises:
+ InvalidSession: If the Session identifier is invalid.
+ InvalidInitData: If the Init Data (or PSSH box) provided is invalid.
+ InvalidLicenseType: If the type_ parameter value is not a License Type. It
+ must be a LicenseType enum, or a string/int representing the enum's keys
+ or values.
+
+ Returns a SignedMessage containing a LicenseRequest message. It's signed with
+ the Private Key of the device provision.
+ """
+ session = self.__sessions.get(session_id)
+ if not session:
+ raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
+
+ if not pssh:
+ raise InvalidInitData("A pssh must be provided.")
+ if not isinstance(pssh, PSSH):
+ raise InvalidInitData(f"Expected pssh to be a {PSSH}, not {pssh!r}")
+
+ if not isinstance(license_type, str):
+ raise InvalidLicenseType(f"Expected license_type to be a {str}, not {license_type!r}")
+ if license_type not in LicenseType.keys():
+ raise InvalidLicenseType(
+ f"Invalid license_type value of '{license_type}'. "
+ f"Available values: {LicenseType.keys()}"
+ )
+
+ if self.device_type == DeviceTypes.ANDROID:
+ # OEMCrypto's request_id seems to be in AES CTR Counter block form with no suffix
+ # Bytes 5-8 does not seem random, in real tests they have been consecutive \x00 or \xFF
+ # Real example: A0DCE548000000000500000000000000
+ request_id = (get_random_bytes(4) + (b"\x00" * 4)) # (?)
+ request_id += session.number.to_bytes(8, "little") # counter
+ # as you can see in the real example, it is stored as uppercase hex and re-encoded
+ # it's really 16 bytes of data, but it's stored as a 32-char HEX string (32 bytes)
+ request_id = request_id.hex().upper().encode()
+ else:
+ request_id = get_random_bytes(16)
+
+ license_request = LicenseRequest(
+ client_id=(
+ self.__client_id
+ ) if not (session.service_certificate and privacy_mode) else None,
+ encrypted_client_id=self.encrypt_client_id(
+ client_id=self.__client_id,
+ service_certificate=session.service_certificate
+ ) if session.service_certificate and privacy_mode else None,
+ content_id=LicenseRequest.ContentIdentification(
+ widevine_pssh_data=LicenseRequest.ContentIdentification.WidevinePsshData(
+ pssh_data=[pssh.init_data], # either a WidevineCencHeader or custom data
+ license_type=license_type,
+ request_id=request_id
+ )
+ ),
+ type="NEW",
+ request_time=int(time.time()),
+ protocol_version="VERSION_2_1",
+ key_control_nonce=random.randrange(1, 2 ** 31),
+ ).SerializeToString()
+
+ signed_license_request = SignedMessage(
+ type="LICENSE_REQUEST",
+ msg=license_request,
+ signature=self.__signer.sign(SHA1.new(license_request))
+ ).SerializeToString()
+
+ session.context[request_id] = self.derive_context(license_request)
+
+ return signed_license_request
+
+ def parse_license(self, session_id: bytes, license_message: Union[SignedMessage, bytes, str]) -> None:
+ """
+ Load Keys from a License Message from a License Server Response.
+
+ License Messages can only be loaded a single time. An InvalidContext error will
+ be raised if you attempt to parse a License Message more than once.
+
+ Parameters:
+ session_id: Session identifier.
+ license_message: A SignedMessage containing a License message.
+
+ Raises:
+ InvalidSession: If the Session identifier is invalid.
+ InvalidLicenseMessage: The License message could not be decoded as a Signed
+ Message or License message.
+ InvalidContext: If the Session has no Context Data. This is likely to happen
+ if the License Challenge was not made by this CDM instance, or was not
+ by this CDM at all. It could also happen if the Session is closed after
+ calling parse_license but not before it got the context data.
+ SignatureMismatch: If the Signature of the License SignedMessage does not
+ match the underlying License.
+ """
+ session = self.__sessions.get(session_id)
+ if not session:
+ raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
+
+ if not license_message:
+ raise InvalidLicenseMessage("Cannot parse an empty license_message")
+
+ if isinstance(license_message, str):
+ try:
+ license_message = base64.b64decode(license_message)
+ except (binascii.Error, binascii.Incomplete) as e:
+ raise InvalidLicenseMessage(f"Could not decode license_message as Base64, {e}")
+
+ if isinstance(license_message, bytes):
+ signed_message = SignedMessage()
+ try:
+ signed_message.ParseFromString(license_message)
+ if signed_message.SerializeToString() != license_message:
+ raise DecodeError(license_message)
+ except DecodeError as e:
+ raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}")
+ license_message = signed_message
+
+ if not isinstance(license_message, SignedMessage):
+ raise InvalidLicenseMessage(f"Expecting license_response to be a SignedMessage, got {license_message!r}")
+
+ if license_message.type != SignedMessage.MessageType.Value("LICENSE"):
+ raise InvalidLicenseMessage(
+ f"Expecting a LICENSE message, not a "
+ f"'{SignedMessage.MessageType.Name(license_message.type)}' message."
+ )
+
+ licence = License()
+ licence.ParseFromString(license_message.msg)
+
+ context = session.context.get(licence.id.request_id)
+ if not context:
+ raise InvalidContext("Cannot parse a license message without first making a license request")
+
+ enc_key, mac_key_server, _ = self.derive_keys(
+ *context,
+ key=self.__decrypter.decrypt(license_message.session_key)
+ )
+
+ # 1. Explicitly use the original `license_message.msg` instead of a re-serializing from `licence`
+ # as some differences may end up in the output due to differences in the proto schema
+ # 2. The oemcrypto_core_message (unknown purpose) is part of the signature algorithm starting with
+ # OEM Crypto API v16 and if available, must be prefixed when HMAC'ing a signature.
+
+ computed_signature = HMAC. \
+ new(mac_key_server, digestmod=SHA256). \
+ update(license_message.oemcrypto_core_message or b""). \
+ update(license_message.msg). \
+ digest()
+
+ if license_message.signature != computed_signature:
+ raise SignatureMismatch("Signature Mismatch on License Message, rejecting license")
+
+ session.keys = [
+ Key.from_key_container(key, enc_key)
+ for key in licence.key
+ ]
+
+ del session.context[licence.id.request_id]
+
+ def get_keys(self, session_id: bytes, type_: Optional[Union[int, str]] = None) -> list[Key]:
+ """
+ Get Keys from the loaded License message.
+
+ Parameters:
+ session_id: Session identifier.
+ type_: (optional) Key Type to filter by and return.
+
+ Raises:
+ InvalidSession: If the Session identifier is invalid.
+ TypeError: If the provided type_ is an unexpected value type.
+ ValueError: If the provided type_ is not a valid Key Type.
+ """
+ session = self.__sessions.get(session_id)
+ if not session:
+ raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
+
+ try:
+ if isinstance(type_, str):
+ type_ = License.KeyContainer.KeyType.Value(type_)
+ elif isinstance(type_, int):
+ License.KeyContainer.KeyType.Name(type_) # only test
+ elif type_ is not None:
+ raise TypeError(f"Expected type_ to be a {License.KeyContainer.KeyType} or int, not {type_!r}")
+ except ValueError as e:
+ raise ValueError(f"Could not parse type_ as a {License.KeyContainer.KeyType}, {e}")
+
+ return [
+ key
+ for key in session.keys
+ if not type_ or key.type == License.KeyContainer.KeyType.Name(type_)
+ ]
+
+ def decrypt(
+ self,
+ session_id: bytes,
+ input_file: Union[Path, str],
+ output_file: Union[Path, str],
+ temp_dir: Optional[Union[Path, str]] = None,
+ exists_ok: bool = False
+ ) -> int:
+ """
+ Decrypt a Widevine-encrypted file using Shaka-packager.
+ Shaka-packager is much more stable than mp4decrypt.
+
+ Parameters:
+ session_id: Session identifier.
+ input_file: File to be decrypted with Session's currently loaded keys.
+ output_file: Location to save decrypted file.
+ temp_dir: Directory to store temporary data while decrypting.
+ exists_ok: Allow overwriting the output_file if it exists.
+
+ Raises:
+ ValueError: If the input or output paths have not been supplied or are
+ invalid.
+ FileNotFoundError: If the input file path does not exist.
+ FileExistsError: If the output file path already exists. Ignored if exists_ok
+ is set to True.
+ NoKeysLoaded: No License was parsed for this Session, No Keys available.
+ EnvironmentError: If the shaka-packager executable could not be found.
+ subprocess.CalledProcessError: If the shaka-packager call returned a non-zero
+ exit code.
+ """
+ if not input_file:
+ raise ValueError("Cannot decrypt nothing, specify an input path")
+ if not output_file:
+ raise ValueError("Cannot decrypt nowhere, specify an output path")
+
+ if not isinstance(input_file, (Path, str)):
+ raise ValueError(f"Expecting input_file to be a Path or str, got {input_file!r}")
+ if not isinstance(output_file, (Path, str)):
+ raise ValueError(f"Expecting output_file to be a Path or str, got {output_file!r}")
+ if not isinstance(temp_dir, (Path, str)) and temp_dir is not None:
+ raise ValueError(f"Expecting temp_dir to be a Path or str, got {temp_dir!r}")
+
+ input_file = Path(input_file)
+ output_file = Path(output_file)
+ temp_dir_ = Path(temp_dir) if temp_dir else None
+
+ if not input_file.is_file():
+ raise FileNotFoundError(f"Input file does not exist, {input_file}")
+ if output_file.is_file() and not exists_ok:
+ raise FileExistsError(f"Output file already exists, {output_file}")
+
+ session = self.__sessions.get(session_id)
+ if not session:
+ raise InvalidSession(f"Session identifier {session_id!r} is invalid.")
+
+ if not session.keys:
+ raise NoKeysLoaded("No Keys are loaded yet, cannot decrypt")
+
+ platform = {"win32": "win", "darwin": "osx"}.get(sys.platform, sys.platform)
+ executable = get_binary_path("shaka-packager", f"packager-{platform}", f"packager-{platform}-x64")
+ if not executable:
+ raise EnvironmentError("Shaka Packager executable not found but is required")
+
+ args = [
+ f"input={input_file},stream=0,output={output_file}",
+ "--enable_raw_key_decryption",
+ "--keys", ",".join([
+ label
+ for i, key in enumerate(session.keys)
+ for label in [
+ f"label=1_{i}:key_id={key.kid.hex}:key={key.key.hex()}",
+ # some services need the KID blanked, e.g., Apple TV+
+ f"label=2_{i}:key_id={'0' * 32}:key={key.key.hex()}"
+ ]
+ if key.type == "CONTENT"
+ ])
+ ]
+
+ if temp_dir_:
+ temp_dir_.mkdir(parents=True, exist_ok=True)
+ args.extend(["--temp_dir", str(temp_dir_)])
+
+ return subprocess.check_call([executable, *args])
+
+ @staticmethod
+ def encrypt_client_id(
+ client_id: ClientIdentification,
+ service_certificate: Union[SignedDrmCertificate, DrmCertificate],
+ key: Optional[bytes] = None,
+ iv: Optional[bytes] = None
+ ) -> EncryptedClientIdentification:
+ """Encrypt the Client ID with the Service's Privacy Certificate."""
+ privacy_key = key or get_random_bytes(16)
+ privacy_iv = iv or get_random_bytes(16)
+
+ if isinstance(service_certificate, SignedDrmCertificate):
+ drm_certificate = DrmCertificate()
+ drm_certificate.ParseFromString(service_certificate.drm_certificate)
+ service_certificate = drm_certificate
+ if not isinstance(service_certificate, DrmCertificate):
+ raise ValueError(f"Expecting Service Certificate to be a DrmCertificate, not {service_certificate!r}")
+
+ encrypted_client_id = EncryptedClientIdentification(
+ provider_id=service_certificate.provider_id,
+ service_certificate_serial_number=service_certificate.serial_number,
+ encrypted_client_id=AES.
+ new(privacy_key, AES.MODE_CBC, privacy_iv).
+ encrypt(Padding.pad(client_id.SerializeToString(), 16)),
+ encrypted_client_id_iv=privacy_iv,
+ encrypted_privacy_key=PKCS1_OAEP.
+ new(RSA.importKey(service_certificate.public_key)).
+ encrypt(privacy_key)
+ )
+
+ return encrypted_client_id
+
+ @staticmethod
+ def derive_context(message: bytes) -> tuple[bytes, bytes]:
+ """Returns 2 Context Data used for computing the AES Encryption and HMAC Keys."""
+
+ def _get_enc_context(msg: bytes) -> bytes:
+ label = b"ENCRYPTION"
+ key_size = 16 * 8 # 128-bit
+ return label + b"\x00" + msg + key_size.to_bytes(4, "big")
+
+ def _get_mac_context(msg: bytes) -> bytes:
+ label = b"AUTHENTICATION"
+ key_size = 32 * 8 * 2 # 512-bit
+ return label + b"\x00" + msg + key_size.to_bytes(4, "big")
+
+ return _get_enc_context(message), _get_mac_context(message)
+
+ @staticmethod
+ def derive_keys(enc_context: bytes, mac_context: bytes, key: bytes) -> tuple[bytes, bytes, bytes]:
+ """
+ Returns 3 keys derived from the input message.
+ Key can either be a pre-provision device aes key, provision key, or a session key.
+
+ For provisioning:
+ - enc: aes key used for unwrapping RSA key out of response
+ - mac_key_server: hmac-sha256 key used for verifying provisioning response
+ - mac_key_client: hmac-sha256 key used for signing provisioning request
+
+ When used with a session key:
+ - enc: decrypting content and other keys
+ - mac_key_server: verifying response
+ - mac_key_client: renewals
+
+ With key as pre-provision device key, it can be used to provision and get an
+ RSA device key and token/cert with key as session key (OAEP wrapped with the
+ post-provision RSA device key), it can be used to decrypt content and signing
+ keys and verify licenses.
+ """
+
+ def _derive(session_key: bytes, context: bytes, counter: int) -> bytes:
+ return CMAC. \
+ new(session_key, ciphermod=AES). \
+ update(counter.to_bytes(1, "big") + context). \
+ digest()
+
+ enc_key = _derive(key, enc_context, 1)
+ mac_key_server = _derive(key, mac_context, 1)
+ mac_key_server += _derive(key, mac_context, 2)
+ mac_key_client = _derive(key, mac_context, 3)
+ mac_key_client += _derive(key, mac_context, 4)
+
+ return enc_key, mac_key_server, mac_key_client
+
+
+__all__ = ("Cdm",)
diff --git a/pywidevine/device.py b/pywidevine/device.py
new file mode 100644
index 0000000..afa438b
--- /dev/null
+++ b/pywidevine/device.py
@@ -0,0 +1,240 @@
+from __future__ import annotations
+
+import base64
+import logging
+from enum import Enum
+from pathlib import Path
+from typing import Any, Optional, Union
+
+from construct import BitStruct, Bytes, Const, ConstructError, Container
+from construct import Enum as CEnum
+from construct import Int8ub, Int16ub
+from construct import Optional as COptional
+from construct import Padded, Padding, Struct, this
+from Crypto.PublicKey import RSA
+from google.protobuf.message import DecodeError
+
+from pywidevine.license_protocol_pb2 import ClientIdentification, DrmCertificate, FileHashes, SignedDrmCertificate
+
+
+class DeviceTypes(Enum):
+ CHROME = 1
+ ANDROID = 2
+
+
+class _Structures:
+ magic = Const(b"WVD")
+
+ header = Struct(
+ "signature" / magic,
+ "version" / Int8ub
+ )
+
+ # - Removed vmp and vmp_len as it should already be within the Client ID
+ v2 = Struct(
+ "signature" / magic,
+ "version" / Const(Int8ub, 2),
+ "type_" / CEnum(
+ Int8ub,
+ **{t.name: t.value for t in DeviceTypes}
+ ),
+ "security_level" / Int8ub,
+ "flags" / Padded(1, COptional(BitStruct(
+ # no per-device flags yet
+ Padding(8)
+ ))),
+ "private_key_len" / Int16ub,
+ "private_key" / Bytes(this.private_key_len),
+ "client_id_len" / Int16ub,
+ "client_id" / Bytes(this.client_id_len)
+ )
+
+ # - Removed system_id as it can be retrieved from the Client ID's DRM Certificate
+ v1 = Struct(
+ "signature" / magic,
+ "version" / Const(Int8ub, 1),
+ "type_" / CEnum(
+ Int8ub,
+ **{t.name: t.value for t in DeviceTypes}
+ ),
+ "security_level" / Int8ub,
+ "flags" / Padded(1, COptional(BitStruct(
+ # no per-device flags yet
+ Padding(8)
+ ))),
+ "private_key_len" / Int16ub,
+ "private_key" / Bytes(this.private_key_len),
+ "client_id_len" / Int16ub,
+ "client_id" / Bytes(this.client_id_len),
+ "vmp_len" / Int16ub,
+ "vmp" / Bytes(this.vmp_len)
+ )
+
+
+class Device:
+ Structures = _Structures
+ supported_structure = Structures.v2
+
+ def __init__(
+ self,
+ *_: Any,
+ type_: DeviceTypes,
+ security_level: int,
+ flags: Optional[dict],
+ private_key: Optional[bytes],
+ client_id: Optional[bytes],
+ **__: Any
+ ):
+ """
+ This is the device key data that is needed for the CDM (Content Decryption Module).
+
+ Parameters:
+ type_: Device Type
+ security_level: Security level from 1 (the highest ranking) to 3 (the lowest ranking)
+ flags: Extra flags
+ private_key: Device Private Key
+ client_id: Device Client Identification Blob
+ """
+ # *_,*__ is to ignore unwanted args, like signature and version from the struct
+
+ if not client_id:
+ raise ValueError("Client ID is required, the WVD does not contain one or is malformed.")
+ if not private_key:
+ raise ValueError("Private Key is required, the WVD does not contain one or is malformed.")
+
+ self.type = DeviceTypes[type_] if isinstance(type_, str) else type_
+ self.security_level = security_level
+ self.flags = flags or {}
+ self.private_key = RSA.importKey(private_key)
+ self.client_id = ClientIdentification()
+ try:
+ self.client_id.ParseFromString(client_id)
+ if self.client_id.SerializeToString() != client_id:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ raise DecodeError(f"Failed to parse client_id as a ClientIdentification, {e}")
+
+ self.vmp = FileHashes()
+ if self.client_id.vmp_data:
+ try:
+ self.vmp.ParseFromString(self.client_id.vmp_data)
+ if self.vmp.SerializeToString() != self.client_id.vmp_data:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ raise DecodeError(f"Failed to parse Client ID's VMP data as a FileHashes, {e}")
+
+ signed_drm_certificate = SignedDrmCertificate()
+ drm_certificate = DrmCertificate()
+
+ try:
+ signed_drm_certificate.ParseFromString(self.client_id.token)
+ if signed_drm_certificate.SerializeToString() != self.client_id.token:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ raise DecodeError(f"Failed to parse the Signed DRM Certificate of the Client ID, {e}")
+
+ try:
+ drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate)
+ if drm_certificate.SerializeToString() != signed_drm_certificate.drm_certificate:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ raise DecodeError(f"Failed to parse the DRM Certificate of the Client ID, {e}")
+
+ self.system_id = drm_certificate.system_id
+
+ def __repr__(self) -> str:
+ return "{name}({items})".format(
+ name=self.__class__.__name__,
+ items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()])
+ )
+
+ @classmethod
+ def loads(cls, data: Union[bytes, str]) -> 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}")
+ return cls(**cls.supported_structure.parse(data))
+
+ @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(**cls.supported_structure.parse_stream(f))
+
+ def dumps(self) -> bytes:
+ private_key = self.private_key.export_key("DER") if self.private_key else None
+ return self.supported_structure.build(dict(
+ version=2,
+ type_=self.type.value,
+ security_level=self.security_level,
+ flags=self.flags,
+ private_key_len=len(private_key) if private_key else 0,
+ private_key=private_key,
+ client_id_len=len(self.client_id.SerializeToString()) if self.client_id else 0,
+ client_id=self.client_id.SerializeToString() if self.client_id else None
+ ))
+
+ 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())
+
+ @classmethod
+ def migrate(cls, data: Union[bytes, str]) -> 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}")
+
+ header = _Structures.header.parse(data)
+ if header.version == 2:
+ raise ValueError("Device Data is already migrated to the latest version.")
+ if header.version == 0 or header.version > 2:
+ # we have never used version 0, likely data that just so happened to use the WVD magic
+ raise ValueError("Device Data does not seem to be a WVD file (v0).")
+
+ if header.version == 1: # v1 to v2
+ v1_struct = _Structures.v1.parse(data)
+ v1_struct.version = 2 # update version to 2 to allow loading
+ v1_struct.flags = Container() # blank flags that may have been used in v1
+
+ vmp = FileHashes()
+ if v1_struct.vmp:
+ try:
+ vmp.ParseFromString(v1_struct.vmp)
+ if vmp.SerializeToString() != v1_struct.vmp:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}")
+ v1_struct.vmp = vmp
+
+ client_id = ClientIdentification()
+ try:
+ client_id.ParseFromString(v1_struct.client_id)
+ if client_id.SerializeToString() != v1_struct.client_id:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ raise DecodeError(f"Failed to parse VMP data as FileHashes, {e}")
+
+ new_vmp_data = v1_struct.vmp.SerializeToString()
+ if client_id.vmp_data and client_id.vmp_data != new_vmp_data:
+ logging.getLogger("migrate").warning("Client ID already has Verified Media Path data")
+ client_id.vmp_data = new_vmp_data
+ v1_struct.client_id = client_id.SerializeToString()
+
+ try:
+ data = _Structures.v2.build(v1_struct)
+ except ConstructError as e:
+ raise ValueError(f"Migration failed, {e}")
+
+ try:
+ return cls.loads(data)
+ except ConstructError as e:
+ raise ValueError(f"Device Data seems to be corrupt or invalid, or migration failed, {e}")
+
+
+__all__ = ("Device", "DeviceTypes")
diff --git a/pywidevine/exceptions.py b/pywidevine/exceptions.py
new file mode 100644
index 0000000..3d6c5c7
--- /dev/null
+++ b/pywidevine/exceptions.py
@@ -0,0 +1,38 @@
+class PyWidevineException(Exception):
+ """Exceptions used by pywidevine."""
+
+
+class TooManySessions(PyWidevineException):
+ """Too many Sessions are open."""
+
+
+class InvalidSession(PyWidevineException):
+ """No Session is open with the specified identifier."""
+
+
+class InvalidInitData(PyWidevineException):
+ """The Widevine Cenc Header Data is invalid or empty."""
+
+
+class InvalidLicenseType(PyWidevineException):
+ """The License Type is an Invalid Value."""
+
+
+class InvalidLicenseMessage(PyWidevineException):
+ """The License Message is Invalid or Missing."""
+
+
+class InvalidContext(PyWidevineException):
+ """The Context is Invalid or Missing."""
+
+
+class SignatureMismatch(PyWidevineException):
+ """The Signature did not match."""
+
+
+class NoKeysLoaded(PyWidevineException):
+ """No License was parsed for this Session, No Keys available."""
+
+
+class DeviceMismatch(PyWidevineException):
+ """The Remote CDMs Device information and the APIs Device information did not match."""
diff --git a/pywidevine/key.py b/pywidevine/key.py
new file mode 100644
index 0000000..c5f8b31
--- /dev/null
+++ b/pywidevine/key.py
@@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import base64
+from typing import Optional, Union
+from uuid import UUID
+
+from Crypto.Cipher import AES
+from Crypto.Util import Padding
+
+from pywidevine.license_protocol_pb2 import License
+
+
+class Key:
+ def __init__(self, type_: str, kid: UUID, key: bytes, permissions: Optional[list[str]] = None):
+ self.type = type_
+ self.kid = kid
+ self.key = key
+ self.permissions = permissions or []
+
+ def __repr__(self) -> str:
+ return "{name}({items})".format(
+ name=self.__class__.__name__,
+ items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()])
+ )
+
+ @classmethod
+ def from_key_container(cls, key: License.KeyContainer, enc_key: bytes) -> Key:
+ """Load Key from a KeyContainer object."""
+ permissions = []
+ if key.type == License.KeyContainer.KeyType.Value("OPERATOR_SESSION"):
+ for descriptor, value in key.operator_session_key_permissions.ListFields():
+ if value == 1:
+ permissions.append(descriptor.name)
+
+ return Key(
+ type_=License.KeyContainer.KeyType.Name(key.type),
+ kid=cls.kid_to_uuid(key.id),
+ key=Padding.unpad(
+ AES.new(enc_key, AES.MODE_CBC, iv=key.iv).decrypt(key.key),
+ 16
+ ),
+ permissions=permissions
+ )
+
+ @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)
+
+
+__all__ = ("Key",)
diff --git a/pywidevine/license_protocol.proto b/pywidevine/license_protocol.proto
new file mode 100644
index 0000000..cd2fe4f
--- /dev/null
+++ b/pywidevine/license_protocol.proto
@@ -0,0 +1,752 @@
+syntax = "proto2";
+
+package pywidevine_license_protocol;
+
+// need this if we are using libprotobuf-cpp-2.3.0-lite
+option optimize_for = LITE_RUNTIME;
+
+option java_package = "com.rlaphoenix.pywidevine.protos";
+
+enum LicenseType {
+ STREAMING = 1;
+ OFFLINE = 2;
+ // License type decision is left to provider.
+ AUTOMATIC = 3;
+}
+
+enum PlatformVerificationStatus {
+ // The platform is not verified.
+ PLATFORM_UNVERIFIED = 0;
+ // Tampering detected on the platform.
+ PLATFORM_TAMPERED = 1;
+ // The platform has been verified by means of software.
+ PLATFORM_SOFTWARE_VERIFIED = 2;
+ // The platform has been verified by means of hardware (e.g. secure boot).
+ PLATFORM_HARDWARE_VERIFIED = 3;
+ // Platform verification was not performed.
+ PLATFORM_NO_VERIFICATION = 4;
+ // Platform and secure storage capability have been verified by means of
+ // software.
+ PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED = 5;
+}
+
+// LicenseIdentification is propagated from LicenseRequest to License,
+// incrementing version with each iteration.
+message LicenseIdentification {
+ optional bytes request_id = 1;
+ optional bytes session_id = 2;
+ optional bytes purchase_id = 3;
+ optional LicenseType type = 4;
+ optional int32 version = 5;
+ optional bytes provider_session_token = 6;
+}
+
+message License {
+ message Policy {
+ // Indicates that playback of the content is allowed.
+ optional bool can_play = 1 [default = false];
+
+ // Indicates that the license may be persisted to non-volatile
+ // storage for offline use.
+ optional bool can_persist = 2 [default = false];
+
+ // Indicates that renewal of this license is allowed.
+ optional bool can_renew = 3 [default = false];
+
+ // For the |*duration*| fields, playback must halt when
+ // license_start_time (seconds since the epoch (UTC)) +
+ // license_duration_seconds is exceeded. A value of 0
+ // indicates that there is no limit to the duration.
+
+ // Indicates the rental window.
+ optional int64 rental_duration_seconds = 4 [default = 0];
+
+ // Indicates the viewing window, once playback has begun.
+ optional int64 playback_duration_seconds = 5 [default = 0];
+
+ // Indicates the time window for this specific license.
+ optional int64 license_duration_seconds = 6 [default = 0];
+
+ // The |renewal*| fields only apply if |can_renew| is true.
+
+ // The window of time, in which playback is allowed to continue while
+ // renewal is attempted, yet unsuccessful due to backend problems with
+ // the license server.
+ optional int64 renewal_recovery_duration_seconds = 7 [default = 0];
+
+ // All renewal requests for this license shall be directed to the
+ // specified URL.
+ optional string renewal_server_url = 8;
+
+ // How many seconds after license_start_time, before renewal is first
+ // attempted.
+ optional int64 renewal_delay_seconds = 9 [default = 0];
+
+ // Specifies the delay in seconds between subsequent license
+ // renewal requests, in case of failure.
+ optional int64 renewal_retry_interval_seconds = 10 [default = 0];
+
+ // Indicates that the license shall be sent for renewal when usage is
+ // started.
+ optional bool renew_with_usage = 11 [default = false];
+
+ // Indicates to client that license renewal and release requests ought to
+ // include ClientIdentification (client_id).
+ optional bool always_include_client_id = 12 [default = false];
+
+ // Duration of grace period before playback_duration_seconds (short window)
+ // goes into effect. Optional.
+ optional int64 play_start_grace_period_seconds = 13 [default = 0];
+
+ // Enables "soft enforcement" of playback_duration_seconds, letting the user
+ // finish playback even if short window expires. Optional.
+ optional bool soft_enforce_playback_duration = 14 [default = false];
+
+ // Enables "soft enforcement" of rental_duration_seconds. Initial playback
+ // must always start before rental duration expires. In order to allow
+ // subsequent playbacks to start after the rental duration expires,
+ // soft_enforce_playback_duration must be true. Otherwise, subsequent
+ // playbacks will not be allowed once rental duration expires. Optional.
+ optional bool soft_enforce_rental_duration = 15 [default = true];
+ }
+
+ message KeyContainer {
+ enum KeyType {
+ SIGNING = 1; // Exactly one key of this type must appear.
+ CONTENT = 2; // Content key.
+ KEY_CONTROL = 3; // Key control block for license renewals. No key.
+ OPERATOR_SESSION = 4; // wrapped keys for auxiliary crypto operations.
+ ENTITLEMENT = 5; // Entitlement keys.
+ OEM_CONTENT = 6; // Partner-specific content key.
+ }
+
+ // The SecurityLevel enumeration allows the server to communicate the level
+ // of robustness required by the client, in order to use the key.
+ enum SecurityLevel {
+ // Software-based whitebox crypto is required.
+ SW_SECURE_CRYPTO = 1;
+
+ // Software crypto and an obfuscated decoder is required.
+ SW_SECURE_DECODE = 2;
+
+ // The key material and crypto operations must be performed within a
+ // hardware backed trusted execution environment.
+ HW_SECURE_CRYPTO = 3;
+
+ // The crypto and decoding of content must be performed within a hardware
+ // backed trusted execution environment.
+ HW_SECURE_DECODE = 4;
+
+ // The crypto, decoding and all handling of the media (compressed and
+ // uncompressed) must be handled within a hardware backed trusted
+ // execution environment.
+ HW_SECURE_ALL = 5;
+ }
+
+ message KeyControl {
+ // |key_control| is documented in:
+ // Widevine Modular DRM Security Integration Guide for CENC
+ // If present, the key control must be communicated to the secure
+ // environment prior to any usage. This message is automatically generated
+ // by the Widevine License Server SDK.
+ optional bytes key_control_block = 1;
+ optional bytes iv = 2;
+ }
+
+ message OutputProtection {
+ // Indicates whether HDCP is required on digital outputs, and which
+ // version should be used.
+ enum HDCP {
+ HDCP_NONE = 0;
+ HDCP_V1 = 1;
+ HDCP_V2 = 2;
+ HDCP_V2_1 = 3;
+ HDCP_V2_2 = 4;
+ HDCP_V2_3 = 5;
+ HDCP_NO_DIGITAL_OUTPUT = 0xff;
+ }
+ optional HDCP hdcp = 1 [default = HDCP_NONE];
+
+ // Indicate the CGMS setting to be inserted on analog output.
+ enum CGMS {
+ CGMS_NONE = 42;
+ COPY_FREE = 0;
+ COPY_ONCE = 2;
+ COPY_NEVER = 3;
+ }
+ optional CGMS cgms_flags = 2 [default = CGMS_NONE];
+
+ enum HdcpSrmRule {
+ HDCP_SRM_RULE_NONE = 0;
+ // In 'required_protection', this means most current SRM is required.
+ // Update the SRM on the device. If update cannot happen,
+ // do not allow the key.
+ // In 'requested_protection', this means most current SRM is requested.
+ // Update the SRM on the device. If update cannot happen,
+ // allow use of the key anyway.
+ CURRENT_SRM = 1;
+ }
+ optional HdcpSrmRule hdcp_srm_rule = 3 [default = HDCP_SRM_RULE_NONE];
+ // Optional requirement to indicate analog output is not allowed.
+ optional bool disable_analog_output = 4 [default = false];
+ // Optional requirement to indicate digital output is not allowed.
+ optional bool disable_digital_output = 5 [default = false];
+ }
+
+ message VideoResolutionConstraint {
+ // Minimum and maximum video resolutions in the range (height x width).
+ optional uint32 min_resolution_pixels = 1;
+ optional uint32 max_resolution_pixels = 2;
+ // Optional output protection requirements for this range. If not
+ // specified, the OutputProtection in the KeyContainer applies.
+ optional OutputProtection required_protection = 3;
+ }
+
+ message OperatorSessionKeyPermissions {
+ // Permissions/key usage flags for operator service keys
+ // (type = OPERATOR_SESSION).
+ optional bool allow_encrypt = 1 [default = false];
+ optional bool allow_decrypt = 2 [default = false];
+ optional bool allow_sign = 3 [default = false];
+ optional bool allow_signature_verify = 4 [default = false];
+ }
+
+ optional bytes id = 1;
+ optional bytes iv = 2;
+ optional bytes key = 3;
+ optional KeyType type = 4;
+ optional SecurityLevel level = 5 [default = SW_SECURE_CRYPTO];
+ optional OutputProtection required_protection = 6;
+ // NOTE: Use of requested_protection is not recommended as it is only
+ // supported on a small number of platforms.
+ optional OutputProtection requested_protection = 7;
+ optional KeyControl key_control = 8;
+ optional OperatorSessionKeyPermissions operator_session_key_permissions = 9;
+ // Optional video resolution constraints. If the video resolution of the
+ // content being decrypted/decoded falls within one of the specified ranges,
+ // the optional required_protections may be applied. Otherwise an error will
+ // be reported.
+ // NOTE: Use of this feature is not recommended, as it is only supported on
+ // a small number of platforms.
+ repeated VideoResolutionConstraint video_resolution_constraints = 10;
+ // Optional flag to indicate the key must only be used if the client
+ // supports anti rollback of the user table. Content provider can query the
+ // client capabilities to determine if the client support this feature.
+ optional bool anti_rollback_usage_table = 11 [default = false];
+ // Optional not limited to commonly known track types such as SD, HD.
+ // It can be some provider defined label to identify the track.
+ optional string track_label = 12;
+ }
+
+ optional LicenseIdentification id = 1;
+ optional Policy policy = 2;
+ repeated KeyContainer key = 3;
+ // Time of the request in seconds (UTC) as set in
+ // LicenseRequest.request_time. If this time is not set in the request,
+ // the local time at the license service is used in this field.
+ optional int64 license_start_time = 4;
+ optional bool remote_attestation_verified = 5 [default = false];
+ // Client token generated by the content provider. Optional.
+ optional bytes provider_client_token = 6;
+ // 4cc code specifying the CENC protection scheme as defined in the CENC 3.0
+ // specification. Propagated from Widevine PSSH box. Optional.
+ optional uint32 protection_scheme = 7;
+ // 8 byte verification field "HDCPDATA" followed by unsigned 32 bit minimum
+ // HDCP SRM version (whether the version is for HDCP1 SRM or HDCP2 SRM
+ // depends on client max_hdcp_version).
+ // Additional details can be found in Widevine Modular DRM Security
+ // Integration Guide for CENC.
+ optional bytes srm_requirement = 8;
+ // If present this contains a signed SRM file (either HDCP1 SRM or HDCP2 SRM
+ // depending on client max_hdcp_version) that should be installed on the
+ // client device.
+ optional bytes srm_update = 9;
+ // Indicates the status of any type of platform verification performed by the
+ // server.
+ optional PlatformVerificationStatus platform_verification_status = 10
+ [default = PLATFORM_NO_VERIFICATION];
+ // IDs of the groups for which keys are delivered in this license, if any.
+ repeated bytes group_ids = 11;
+}
+
+enum ProtocolVersion {
+ VERSION_2_0 = 20;
+ VERSION_2_1 = 21;
+ VERSION_2_2 = 22;
+}
+
+message LicenseRequest {
+ message ContentIdentification {
+ message WidevinePsshData {
+ repeated bytes pssh_data = 1;
+ optional LicenseType license_type = 2;
+ optional bytes request_id = 3; // Opaque, client-specified.
+ }
+
+ message WebmKeyId {
+ optional bytes header = 1;
+ optional LicenseType license_type = 2;
+ optional bytes request_id = 3; // Opaque, client-specified.
+ }
+
+ message ExistingLicense {
+ optional LicenseIdentification license_id = 1;
+ optional int64 seconds_since_started = 2;
+ optional int64 seconds_since_last_played = 3;
+ optional bytes session_usage_table_entry = 4;
+ }
+
+ message InitData {
+ enum InitDataType {
+ CENC = 1;
+ WEBM = 2;
+ }
+
+ optional InitDataType init_data_type = 1 [default = CENC];
+ optional bytes init_data = 2;
+ optional LicenseType license_type = 3;
+ optional bytes request_id = 4;
+ }
+
+ oneof content_id_variant {
+ // Exactly one of these must be present.
+ WidevinePsshData widevine_pssh_data = 1;
+ WebmKeyId webm_key_id = 2;
+ ExistingLicense existing_license = 3;
+ InitData init_data = 4;
+ }
+ }
+
+ enum RequestType {
+ NEW = 1;
+ RENEWAL = 2;
+ RELEASE = 3;
+ }
+
+ // The client_id provides information authenticating the calling device. It
+ // contains the Widevine keybox token that was installed on the device at the
+ // factory. This field or encrypted_client_id below is required for a valid
+ // license request, but both should never be present in the same request.
+ optional ClientIdentification client_id = 1;
+ optional ContentIdentification content_id = 2;
+ optional RequestType type = 3;
+ // Time of the request in seconds (UTC) as set by the client.
+ optional int64 request_time = 4;
+ // Old-style decimal-encoded string key control nonce.
+ optional bytes key_control_nonce_deprecated = 5;
+ optional ProtocolVersion protocol_version = 6 [default = VERSION_2_0];
+ // New-style uint32 key control nonce, please use instead of
+ // key_control_nonce_deprecated.
+ optional uint32 key_control_nonce = 7;
+ // Encrypted ClientIdentification message, used for privacy purposes.
+ optional EncryptedClientIdentification encrypted_client_id = 8;
+}
+
+message MetricData {
+ enum MetricType {
+ // The time spent in the 'stage', specified in microseconds.
+ LATENCY = 1;
+ // The UNIX epoch timestamp at which the 'stage' was first accessed in
+ // microseconds.
+ TIMESTAMP = 2;
+ }
+
+ message TypeValue {
+ optional MetricType type = 1;
+ // The value associated with 'type'. For example if type == LATENCY, the
+ // value would be the time in microseconds spent in this 'stage'.
+ optional int64 value = 2 [default = 0];
+ }
+
+ // 'stage' that is currently processing the SignedMessage. Required.
+ optional string stage_name = 1;
+ // metric and associated value.
+ repeated TypeValue metric_data = 2;
+}
+
+message VersionInfo {
+ // License SDK version reported by the Widevine License SDK. This field
+ // is populated automatically by the SDK.
+ optional string license_sdk_version = 1;
+ // Version of the service hosting the license SDK. This field is optional.
+ // It may be provided by the hosting service.
+ optional string license_service_version = 2;
+}
+
+message SignedMessage {
+ enum MessageType {
+ LICENSE_REQUEST = 1;
+ LICENSE = 2;
+ ERROR_RESPONSE = 3;
+ SERVICE_CERTIFICATE_REQUEST = 4;
+ SERVICE_CERTIFICATE = 5;
+ SUB_LICENSE = 6;
+ CAS_LICENSE_REQUEST = 7;
+ CAS_LICENSE = 8;
+ EXTERNAL_LICENSE_REQUEST = 9;
+ EXTERNAL_LICENSE = 10;
+ }
+
+ enum SessionKeyType {
+ UNDEFINED = 0;
+ WRAPPED_AES_KEY = 1;
+ EPHERMERAL_ECC_PUBLIC_KEY = 2;
+ }
+ optional MessageType type = 1;
+ optional bytes msg = 2;
+ // Required field that contains the signature of the bytes of msg.
+ // For license requests, the signing algorithm is determined by the
+ // certificate contained in the request.
+ // For license responses, the signing algorithm is HMAC with signing key based
+ // on |session_key|.
+ optional bytes signature = 3;
+ // If populated, the contents of this field will be signaled by the
+ // |session_key_type| type. If the |session_key_type| is WRAPPED_AES_KEY the
+ // key is the bytes of an encrypted AES key. If the |session_key_type| is
+ // EPHERMERAL_ECC_PUBLIC_KEY the field contains the bytes of an RFC5208 ASN1
+ // serialized ECC public key.
+ optional bytes session_key = 4;
+ // Remote attestation data which will be present in the initial license
+ // request for ChromeOS client devices operating in verified mode. Remote
+ // attestation challenge data is |msg| field above. Optional.
+ optional bytes remote_attestation = 5;
+
+ repeated MetricData metric_data = 6;
+ // Version information from the SDK and license service. This information is
+ // provided in the license response.
+ optional VersionInfo service_version_info = 7;
+ // Optional field that contains the algorithm type used to generate the
+ // session_key and signature in a LICENSE message.
+ optional SessionKeyType session_key_type = 8 [default = WRAPPED_AES_KEY];
+ // The core message is the simple serialization of fields used by OEMCrypto.
+ // This field was introduced in OEMCrypto API v16.
+ optional bytes oemcrypto_core_message = 9;
+}
+
+enum HashAlgorithmProto {
+ // Unspecified hash algorithm: SHA_256 shall be used for ECC based algorithms
+ // and SHA_1 shall be used otherwise.
+ HASH_ALGORITHM_UNSPECIFIED = 0;
+ HASH_ALGORITHM_SHA_1 = 1;
+ HASH_ALGORITHM_SHA_256 = 2;
+ HASH_ALGORITHM_SHA_384 = 3;
+}
+
+// ClientIdentification message used to authenticate the client device.
+message ClientIdentification {
+ enum TokenType {
+ KEYBOX = 0;
+ DRM_DEVICE_CERTIFICATE = 1;
+ REMOTE_ATTESTATION_CERTIFICATE = 2;
+ OEM_DEVICE_CERTIFICATE = 3;
+ }
+
+ message NameValue {
+ optional string name = 1;
+ optional string value = 2;
+ }
+
+ // Capabilities which not all clients may support. Used for the license
+ // exchange protocol only.
+ message ClientCapabilities {
+ enum HdcpVersion {
+ HDCP_NONE = 0;
+ HDCP_V1 = 1;
+ HDCP_V2 = 2;
+ HDCP_V2_1 = 3;
+ HDCP_V2_2 = 4;
+ HDCP_V2_3 = 5;
+ HDCP_NO_DIGITAL_OUTPUT = 0xff;
+ }
+
+ enum CertificateKeyType {
+ RSA_2048 = 0;
+ RSA_3072 = 1;
+ ECC_SECP256R1 = 2;
+ ECC_SECP384R1 = 3;
+ ECC_SECP521R1 = 4;
+ }
+
+ enum AnalogOutputCapabilities {
+ ANALOG_OUTPUT_UNKNOWN = 0;
+ ANALOG_OUTPUT_NONE = 1;
+ ANALOG_OUTPUT_SUPPORTED = 2;
+ ANALOG_OUTPUT_SUPPORTS_CGMS_A = 3;
+ }
+
+ optional bool client_token = 1 [default = false];
+ optional bool session_token = 2 [default = false];
+ optional bool video_resolution_constraints = 3 [default = false];
+ optional HdcpVersion max_hdcp_version = 4 [default = HDCP_NONE];
+ optional uint32 oem_crypto_api_version = 5;
+ // Client has hardware support for protecting the usage table, such as
+ // storing the generation number in secure memory. For Details, see:
+ // Widevine Modular DRM Security Integration Guide for CENC
+ optional bool anti_rollback_usage_table = 6 [default = false];
+ // The client shall report |srm_version| if available.
+ optional uint32 srm_version = 7;
+ // A device may have SRM data, and report a version, but may not be capable
+ // of updating SRM data.
+ optional bool can_update_srm = 8 [default = false];
+ repeated CertificateKeyType supported_certificate_key_type = 9;
+ optional AnalogOutputCapabilities analog_output_capabilities = 10
+ [default = ANALOG_OUTPUT_UNKNOWN];
+ optional bool can_disable_analog_output = 11 [default = false];
+ // Clients can indicate a performance level supported by OEMCrypto.
+ // This will allow applications and providers to choose an appropriate
+ // quality of content to serve. Currently defined tiers are
+ // 1 (low), 2 (medium) and 3 (high). Any other value indicates that
+ // the resource rating is unavailable or reporting erroneous values
+ // for that device. For details see,
+ // Widevine Modular DRM Security Integration Guide for CENC
+ optional uint32 resource_rating_tier = 12 [default = 0];
+ }
+
+ message ClientCredentials {
+ optional TokenType type = 1 [default = KEYBOX];
+ optional bytes token = 2;
+ }
+
+ // Type of factory-provisioned device root of trust. Optional.
+ optional TokenType type = 1 [default = KEYBOX];
+ // Factory-provisioned device root of trust. Required.
+ optional bytes token = 2;
+ // Optional client information name/value pairs.
+ repeated NameValue client_info = 3;
+ // Client token generated by the content provider. Optional.
+ optional bytes provider_client_token = 4;
+ // Number of licenses received by the client to which the token above belongs.
+ // Only present if client_token is specified.
+ optional uint32 license_counter = 5;
+ // List of non-baseline client capabilities.
+ optional ClientCapabilities client_capabilities = 6;
+ // Serialized VmpData message. Optional.
+ optional bytes vmp_data = 7;
+ // Optional field that may contain additional provisioning credentials.
+ repeated ClientCredentials device_credentials = 8;
+}
+
+// EncryptedClientIdentification message used to hold ClientIdentification
+// messages encrypted for privacy purposes.
+message EncryptedClientIdentification {
+ // Provider ID for which the ClientIdentifcation is encrypted (owner of
+ // service certificate).
+ optional string provider_id = 1;
+ // Serial number for the service certificate for which ClientIdentification is
+ // encrypted.
+ optional bytes service_certificate_serial_number = 2;
+ // Serialized ClientIdentification message, encrypted with the privacy key
+ // using AES-128-CBC with PKCS#5 padding.
+ optional bytes encrypted_client_id = 3;
+ // Initialization vector needed to decrypt encrypted_client_id.
+ optional bytes encrypted_client_id_iv = 4;
+ // AES-128 privacy key, encrypted with the service public key using RSA-OAEP.
+ optional bytes encrypted_privacy_key = 5;
+}
+
+// DRM certificate definition for user devices, intermediate, service, and root
+// certificates.
+message DrmCertificate {
+ enum Type {
+ ROOT = 0; // ProtoBestPractices: ignore.
+ DEVICE_MODEL = 1;
+ DEVICE = 2;
+ SERVICE = 3;
+ PROVISIONER = 4;
+ }
+ enum ServiceType {
+ UNKNOWN_SERVICE_TYPE = 0;
+ LICENSE_SERVER_SDK = 1;
+ LICENSE_SERVER_PROXY_SDK = 2;
+ PROVISIONING_SDK = 3;
+ CAS_PROXY_SDK = 4;
+ }
+ enum Algorithm {
+ UNKNOWN_ALGORITHM = 0;
+ RSA = 1;
+ ECC_SECP256R1 = 2;
+ ECC_SECP384R1 = 3;
+ ECC_SECP521R1 = 4;
+ }
+
+ message EncryptionKey {
+ // Device public key. PKCS#1 ASN.1 DER-encoded. Required.
+ optional bytes public_key = 1;
+ // Required. The algorithm field contains the curve used to create the
+ // |public_key| if algorithm is one of the ECC types.
+ // The |algorithm| is used for both to determine the if the certificate is
+ // ECC or RSA. The |algorithm| also specifies the parameters that were used
+ // to create |public_key| and are used to create an ephemeral session key.
+ optional Algorithm algorithm = 2 [default = RSA];
+ }
+
+ // Type of certificate. Required.
+ optional Type type = 1;
+ // 128-bit globally unique serial number of certificate.
+ // Value is 0 for root certificate. Required.
+ optional bytes serial_number = 2;
+ // POSIX time, in seconds, when the certificate was created. Required.
+ optional uint32 creation_time_seconds = 3;
+ // POSIX time, in seconds, when the certificate should expire. Value of zero
+ // denotes indefinite expiry time. For more information on limited lifespan
+ // DRM certificates see (go/limited-lifespan-drm-certificates).
+ optional uint32 expiration_time_seconds = 12;
+ // Device public key. PKCS#1 ASN.1 DER-encoded. Required.
+ optional bytes public_key = 4;
+ // Widevine system ID for the device. Required for intermediate and
+ // user device certificates.
+ optional uint32 system_id = 5;
+ // Deprecated field, which used to indicate whether the device was a test
+ // (non-production) device. The test_device field in ProvisionedDeviceInfo
+ // below should be observed instead.
+ optional bool test_device_deprecated = 6 [deprecated = true];
+ // Service identifier (web origin) for the provider which owns the
+ // certificate. Required for service and provisioner certificates.
+ optional string provider_id = 7;
+ // This field is used only when type = SERVICE to specify which SDK uses
+ // service certificate. This repeated field is treated as a set. A certificate
+ // may be used for the specified service SDK if the appropriate ServiceType
+ // is specified in this field.
+ repeated ServiceType service_types = 8;
+ // Required. The algorithm field contains the curve used to create the
+ // |public_key| if algorithm is one of the ECC types.
+ // The |algorithm| is used for both to determine the if the certificate is ECC
+ // or RSA. The |algorithm| also specifies the parameters that were used to
+ // create |public_key| and are used to create an ephemeral session key.
+ optional Algorithm algorithm = 9 [default = RSA];
+ // Optional. May be present in DEVICE certificate types. This is the root
+ // of trust identifier that holds an encrypted value that identifies the
+ // keybox or other root of trust that was used to provision a DEVICE drm
+ // certificate.
+ optional bytes rot_id = 10;
+ // Optional. May be present in devices that explicitly support dual keys. When
+ // present the |public_key| is used for verification of received license
+ // request messages.
+ optional EncryptionKey encryption_key = 11;
+}
+
+// DrmCertificate signed by a higher (CA) DRM certificate.
+message SignedDrmCertificate {
+ // Serialized certificate. Required.
+ optional bytes drm_certificate = 1;
+ // Signature of certificate. Signed with root or intermediate
+ // certificate specified below. Required.
+ optional bytes signature = 2;
+ // SignedDrmCertificate used to sign this certificate.
+ optional SignedDrmCertificate signer = 3;
+ // Optional field that indicates the hash algorithm used in signature scheme.
+ optional HashAlgorithmProto hash_algorithm = 4;
+}
+
+message WidevinePsshData {
+ enum Type {
+ SINGLE = 0; // Single PSSH to be used to retrieve content keys.
+ ENTITLEMENT = 1; // Primary PSSH used to retrieve entitlement keys.
+ ENTITLED_KEY = 2; // Secondary PSSH containing entitled key(s).
+ }
+
+ message EntitledKey {
+ // ID of entitlement key used for wrapping |key|.
+ optional bytes entitlement_key_id = 1;
+ // ID of the entitled key.
+ optional bytes key_id = 2;
+ // Wrapped key. Required.
+ optional bytes key = 3;
+ // IV used for wrapping |key|. Required.
+ optional bytes iv = 4;
+ // Size of entitlement key used for wrapping |key|.
+ optional uint32 entitlement_key_size_bytes = 5 [default = 32];
+ }
+
+ // Entitlement or content key IDs. Can onnly present in SINGLE or ENTITLEMENT
+ // PSSHs. May be repeated to facilitate delivery of multiple keys in a
+ // single license. Cannot be used in conjunction with content_id or
+ // group_ids, which are the preferred mechanism.
+ repeated bytes key_ids = 2;
+
+ // Content identifier which may map to multiple entitlement or content key
+ // IDs to facilitate the delivery of multiple keys in a single license.
+ // Cannot be present in conjunction with key_ids, but if used must be in all
+ // PSSHs.
+ optional bytes content_id = 4;
+
+ // Crypto period index, for media using key rotation. Always corresponds to
+ // The content key period. This means that if using entitlement licensing
+ // the ENTITLED_KEY PSSHs will have sequential crypto_period_index's, whereas
+ // the ENTITELEMENT PSSHs will have gaps in the sequence. Required if doing
+ // key rotation.
+ optional uint32 crypto_period_index = 7;
+
+ // Protection scheme identifying the encryption algorithm. The protection
+ // scheme is represented as a uint32 value. The uint32 contains 4 bytes each
+ // representing a single ascii character in one of the 4CC protection scheme
+ // values. To be deprecated in favor of signaling from content.
+ // 'cenc' (AES-CTR) protection_scheme = 0x63656E63,
+ // 'cbc1' (AES-CBC) protection_scheme = 0x63626331,
+ // 'cens' (AES-CTR pattern encryption) protection_scheme = 0x63656E73,
+ // 'cbcs' (AES-CBC pattern encryption) protection_scheme = 0x63626373.
+ optional uint32 protection_scheme = 9;
+
+ // Optional. For media using key rotation, this represents the duration
+ // of each crypto period in seconds.
+ optional uint32 crypto_period_seconds = 10;
+
+ // Type of PSSH. Required if not SINGLE.
+ optional Type type = 11 [default = SINGLE];
+
+ // Key sequence for Widevine-managed keys. Optional.
+ optional uint32 key_sequence = 12;
+
+ // Group identifiers for all groups to which the content belongs. This can
+ // be used to deliver licenses to unlock multiple titles / channels.
+ // Optional, and may only be present in ENTITLEMENT and ENTITLED_KEY PSSHs, and
+ // not in conjunction with key_ids.
+ repeated bytes group_ids = 13;
+
+ // Copy/copies of the content key used to decrypt the media stream in which
+ // the PSSH box is embedded, each wrapped with a different entitlement key.
+ // May also contain sub-licenses to support devices with OEMCrypto 13 or
+ // older. May be repeated if using group entitlement keys. Present only in
+ // PSSHs of type ENTITLED_KEY.
+ repeated EntitledKey entitled_keys = 14;
+
+ // Video feature identifier, which is used in conjunction with |content_id|
+ // to determine the set of keys to be returned in the license. Cannot be
+ // present in conjunction with |key_ids|.
+ // Current values are "HDR".
+ optional string video_feature = 15;
+
+ //////////////////////////// Deprecated Fields ////////////////////////////
+ enum Algorithm {
+ UNENCRYPTED = 0;
+ AESCTR = 1;
+ };
+ optional Algorithm algorithm = 1 [deprecated = true];
+
+ // Content provider name.
+ optional string provider = 3 [deprecated = true];
+
+ // Track type. Acceptable values are SD, HD and AUDIO. Used to
+ // differentiate content keys used by an asset.
+ optional string track_type = 5 [deprecated = true];
+
+ // The name of a registered policy to be used for this asset.
+ optional string policy = 6 [deprecated = true];
+
+ // Optional protected context for group content. The grouped_license is a
+ // serialized SignedMessage.
+ optional bytes grouped_license = 8 [deprecated = true];
+}
+
+// File Hashes for Verified Media Path (VMP) support.
+message FileHashes {
+ message Signature {
+ optional string filename = 1;
+ optional bool test_signing = 2; //0 - release, 1 - testing
+ optional bytes SHA512Hash = 3;
+ optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file
+ optional bytes signature = 5;
+ }
+ optional bytes signer = 1;
+ repeated Signature signatures = 2;
+}
diff --git a/pywidevine/license_protocol_pb2.py b/pywidevine/license_protocol_pb2.py
new file mode 100644
index 0000000..92ae262
--- /dev/null
+++ b/pywidevine/license_protocol_pb2.py
@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: license_protocol.proto
+# Protobuf Python Version: 4.25.1
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x16license_protocol.proto\x12\x1bpywidevine_license_protocol\"\xbd\x01\n\x15LicenseIdentification\x12\x12\n\nrequest_id\x18\x01 \x01(\x0c\x12\x12\n\nsession_id\x18\x02 \x01(\x0c\x12\x13\n\x0bpurchase_id\x18\x03 \x01(\x0c\x12\x36\n\x04type\x18\x04 \x01(\x0e\x32(.pywidevine_license_protocol.LicenseType\x12\x0f\n\x07version\x18\x05 \x01(\x05\x12\x1e\n\x16provider_session_token\x18\x06 \x01(\x0c\"\xf1\x18\n\x07License\x12>\n\x02id\x18\x01 \x01(\x0b\x32\x32.pywidevine_license_protocol.LicenseIdentification\x12;\n\x06policy\x18\x02 \x01(\x0b\x32+.pywidevine_license_protocol.License.Policy\x12>\n\x03key\x18\x03 \x03(\x0b\x32\x31.pywidevine_license_protocol.License.KeyContainer\x12\x1a\n\x12license_start_time\x18\x04 \x01(\x03\x12*\n\x1bremote_attestation_verified\x18\x05 \x01(\x08:\x05\x66\x61lse\x12\x1d\n\x15provider_client_token\x18\x06 \x01(\x0c\x12\x19\n\x11protection_scheme\x18\x07 \x01(\r\x12\x17\n\x0fsrm_requirement\x18\x08 \x01(\x0c\x12\x12\n\nsrm_update\x18\t \x01(\x0c\x12w\n\x1cplatform_verification_status\x18\n \x01(\x0e\x32\x37.pywidevine_license_protocol.PlatformVerificationStatus:\x18PLATFORM_NO_VERIFICATION\x12\x11\n\tgroup_ids\x18\x0b \x03(\x0c\x1a\xae\x04\n\x06Policy\x12\x17\n\x08\x63\x61n_play\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x1a\n\x0b\x63\x61n_persist\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x18\n\tcan_renew\x18\x03 \x01(\x08:\x05\x66\x61lse\x12\"\n\x17rental_duration_seconds\x18\x04 \x01(\x03:\x01\x30\x12$\n\x19playback_duration_seconds\x18\x05 \x01(\x03:\x01\x30\x12#\n\x18license_duration_seconds\x18\x06 \x01(\x03:\x01\x30\x12,\n!renewal_recovery_duration_seconds\x18\x07 \x01(\x03:\x01\x30\x12\x1a\n\x12renewal_server_url\x18\x08 \x01(\t\x12 \n\x15renewal_delay_seconds\x18\t \x01(\x03:\x01\x30\x12)\n\x1erenewal_retry_interval_seconds\x18\n \x01(\x03:\x01\x30\x12\x1f\n\x10renew_with_usage\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\'\n\x18\x61lways_include_client_id\x18\x0c \x01(\x08:\x05\x66\x61lse\x12*\n\x1fplay_start_grace_period_seconds\x18\r \x01(\x03:\x01\x30\x12-\n\x1esoft_enforce_playback_duration\x18\x0e \x01(\x08:\x05\x66\x61lse\x12*\n\x1csoft_enforce_rental_duration\x18\x0f \x01(\x08:\x04true\x1a\xbc\x10\n\x0cKeyContainer\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\n\n\x02iv\x18\x02 \x01(\x0c\x12\x0b\n\x03key\x18\x03 \x01(\x0c\x12G\n\x04type\x18\x04 \x01(\x0e\x32\x39.pywidevine_license_protocol.License.KeyContainer.KeyType\x12`\n\x05level\x18\x05 \x01(\x0e\x32?.pywidevine_license_protocol.License.KeyContainer.SecurityLevel:\x10SW_SECURE_CRYPTO\x12_\n\x13required_protection\x18\x06 \x01(\x0b\x32\x42.pywidevine_license_protocol.License.KeyContainer.OutputProtection\x12`\n\x14requested_protection\x18\x07 \x01(\x0b\x32\x42.pywidevine_license_protocol.License.KeyContainer.OutputProtection\x12Q\n\x0bkey_control\x18\x08 \x01(\x0b\x32<.pywidevine_license_protocol.License.KeyContainer.KeyControl\x12y\n operator_session_key_permissions\x18\t \x01(\x0b\x32O.pywidevine_license_protocol.License.KeyContainer.OperatorSessionKeyPermissions\x12q\n\x1cvideo_resolution_constraints\x18\n \x03(\x0b\x32K.pywidevine_license_protocol.License.KeyContainer.VideoResolutionConstraint\x12(\n\x19\x61nti_rollback_usage_table\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x13\n\x0btrack_label\x18\x0c \x01(\t\x1a\x33\n\nKeyControl\x12\x19\n\x11key_control_block\x18\x01 \x01(\x0c\x12\n\n\x02iv\x18\x02 \x01(\x0c\x1a\x9c\x05\n\x10OutputProtection\x12`\n\x04hdcp\x18\x01 \x01(\x0e\x32G.pywidevine_license_protocol.License.KeyContainer.OutputProtection.HDCP:\tHDCP_NONE\x12\x66\n\ncgms_flags\x18\x02 \x01(\x0e\x32G.pywidevine_license_protocol.License.KeyContainer.OutputProtection.CGMS:\tCGMS_NONE\x12y\n\rhdcp_srm_rule\x18\x03 \x01(\x0e\x32N.pywidevine_license_protocol.License.KeyContainer.OutputProtection.HdcpSrmRule:\x12HDCP_SRM_RULE_NONE\x12$\n\x15\x64isable_analog_output\x18\x04 \x01(\x08:\x05\x66\x61lse\x12%\n\x16\x64isable_digital_output\x18\x05 \x01(\x08:\x05\x66\x61lse\"y\n\x04HDCP\x12\r\n\tHDCP_NONE\x10\x00\x12\x0b\n\x07HDCP_V1\x10\x01\x12\x0b\n\x07HDCP_V2\x10\x02\x12\r\n\tHDCP_V2_1\x10\x03\x12\r\n\tHDCP_V2_2\x10\x04\x12\r\n\tHDCP_V2_3\x10\x05\x12\x1b\n\x16HDCP_NO_DIGITAL_OUTPUT\x10\xff\x01\"C\n\x04\x43GMS\x12\r\n\tCGMS_NONE\x10*\x12\r\n\tCOPY_FREE\x10\x00\x12\r\n\tCOPY_ONCE\x10\x02\x12\x0e\n\nCOPY_NEVER\x10\x03\"6\n\x0bHdcpSrmRule\x12\x16\n\x12HDCP_SRM_RULE_NONE\x10\x00\x12\x0f\n\x0b\x43URRENT_SRM\x10\x01\x1a\xba\x01\n\x19VideoResolutionConstraint\x12\x1d\n\x15min_resolution_pixels\x18\x01 \x01(\r\x12\x1d\n\x15max_resolution_pixels\x18\x02 \x01(\r\x12_\n\x13required_protection\x18\x03 \x01(\x0b\x32\x42.pywidevine_license_protocol.License.KeyContainer.OutputProtection\x1a\x9d\x01\n\x1dOperatorSessionKeyPermissions\x12\x1c\n\rallow_encrypt\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x1c\n\rallow_decrypt\x18\x02 \x01(\x08:\x05\x66\x61lse\x12\x19\n\nallow_sign\x18\x03 \x01(\x08:\x05\x66\x61lse\x12%\n\x16\x61llow_signature_verify\x18\x04 \x01(\x08:\x05\x66\x61lse\"l\n\x07KeyType\x12\x0b\n\x07SIGNING\x10\x01\x12\x0b\n\x07\x43ONTENT\x10\x02\x12\x0f\n\x0bKEY_CONTROL\x10\x03\x12\x14\n\x10OPERATOR_SESSION\x10\x04\x12\x0f\n\x0b\x45NTITLEMENT\x10\x05\x12\x0f\n\x0bOEM_CONTENT\x10\x06\"z\n\rSecurityLevel\x12\x14\n\x10SW_SECURE_CRYPTO\x10\x01\x12\x14\n\x10SW_SECURE_DECODE\x10\x02\x12\x14\n\x10HW_SECURE_CRYPTO\x10\x03\x12\x14\n\x10HW_SECURE_DECODE\x10\x04\x12\x11\n\rHW_SECURE_ALL\x10\x05\"\xbd\r\n\x0eLicenseRequest\x12\x44\n\tclient_id\x18\x01 \x01(\x0b\x32\x31.pywidevine_license_protocol.ClientIdentification\x12U\n\ncontent_id\x18\x02 \x01(\x0b\x32\x41.pywidevine_license_protocol.LicenseRequest.ContentIdentification\x12\x45\n\x04type\x18\x03 \x01(\x0e\x32\x37.pywidevine_license_protocol.LicenseRequest.RequestType\x12\x14\n\x0crequest_time\x18\x04 \x01(\x03\x12$\n\x1ckey_control_nonce_deprecated\x18\x05 \x01(\x0c\x12S\n\x10protocol_version\x18\x06 \x01(\x0e\x32,.pywidevine_license_protocol.ProtocolVersion:\x0bVERSION_2_0\x12\x19\n\x11key_control_nonce\x18\x07 \x01(\r\x12W\n\x13\x65ncrypted_client_id\x18\x08 \x01(\x0b\x32:.pywidevine_license_protocol.EncryptedClientIdentification\x1a\x8f\t\n\x15\x43ontentIdentification\x12p\n\x12widevine_pssh_data\x18\x01 \x01(\x0b\x32R.pywidevine_license_protocol.LicenseRequest.ContentIdentification.WidevinePsshDataH\x00\x12\x62\n\x0bwebm_key_id\x18\x02 \x01(\x0b\x32K.pywidevine_license_protocol.LicenseRequest.ContentIdentification.WebmKeyIdH\x00\x12m\n\x10\x65xisting_license\x18\x03 \x01(\x0b\x32Q.pywidevine_license_protocol.LicenseRequest.ContentIdentification.ExistingLicenseH\x00\x12_\n\tinit_data\x18\x04 \x01(\x0b\x32J.pywidevine_license_protocol.LicenseRequest.ContentIdentification.InitDataH\x00\x1ay\n\x10WidevinePsshData\x12\x11\n\tpssh_data\x18\x01 \x03(\x0c\x12>\n\x0clicense_type\x18\x02 \x01(\x0e\x32(.pywidevine_license_protocol.LicenseType\x12\x12\n\nrequest_id\x18\x03 \x01(\x0c\x1ao\n\tWebmKeyId\x12\x0e\n\x06header\x18\x01 \x01(\x0c\x12>\n\x0clicense_type\x18\x02 \x01(\x0e\x32(.pywidevine_license_protocol.LicenseType\x12\x12\n\nrequest_id\x18\x03 \x01(\x0c\x1a\xbe\x01\n\x0f\x45xistingLicense\x12\x46\n\nlicense_id\x18\x01 \x01(\x0b\x32\x32.pywidevine_license_protocol.LicenseIdentification\x12\x1d\n\x15seconds_since_started\x18\x02 \x01(\x03\x12!\n\x19seconds_since_last_played\x18\x03 \x01(\x03\x12!\n\x19session_usage_table_entry\x18\x04 \x01(\x0c\x1a\x8c\x02\n\x08InitData\x12u\n\x0einit_data_type\x18\x01 \x01(\x0e\x32W.pywidevine_license_protocol.LicenseRequest.ContentIdentification.InitData.InitDataType:\x04\x43\x45NC\x12\x11\n\tinit_data\x18\x02 \x01(\x0c\x12>\n\x0clicense_type\x18\x03 \x01(\x0e\x32(.pywidevine_license_protocol.LicenseType\x12\x12\n\nrequest_id\x18\x04 \x01(\x0c\"\"\n\x0cInitDataType\x12\x08\n\x04\x43\x45NC\x10\x01\x12\x08\n\x04WEBM\x10\x02\x42\x14\n\x12\x63ontent_id_variant\"0\n\x0bRequestType\x12\x07\n\x03NEW\x10\x01\x12\x0b\n\x07RENEWAL\x10\x02\x12\x0b\n\x07RELEASE\x10\x03\"\xf3\x01\n\nMetricData\x12\x12\n\nstage_name\x18\x01 \x01(\t\x12\x46\n\x0bmetric_data\x18\x02 \x03(\x0b\x32\x31.pywidevine_license_protocol.MetricData.TypeValue\x1a_\n\tTypeValue\x12@\n\x04type\x18\x01 \x01(\x0e\x32\x32.pywidevine_license_protocol.MetricData.MetricType\x12\x10\n\x05value\x18\x02 \x01(\x03:\x01\x30\"(\n\nMetricType\x12\x0b\n\x07LATENCY\x10\x01\x12\r\n\tTIMESTAMP\x10\x02\"K\n\x0bVersionInfo\x12\x1b\n\x13license_sdk_version\x18\x01 \x01(\t\x12\x1f\n\x17license_service_version\x18\x02 \x01(\t\"\xf6\x05\n\rSignedMessage\x12\x44\n\x04type\x18\x01 \x01(\x0e\x32\x36.pywidevine_license_protocol.SignedMessage.MessageType\x12\x0b\n\x03msg\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\x13\n\x0bsession_key\x18\x04 \x01(\x0c\x12\x1a\n\x12remote_attestation\x18\x05 \x01(\x0c\x12<\n\x0bmetric_data\x18\x06 \x03(\x0b\x32\'.pywidevine_license_protocol.MetricData\x12\x46\n\x14service_version_info\x18\x07 \x01(\x0b\x32(.pywidevine_license_protocol.VersionInfo\x12\x64\n\x10session_key_type\x18\x08 \x01(\x0e\x32\x39.pywidevine_license_protocol.SignedMessage.SessionKeyType:\x0fWRAPPED_AES_KEY\x12\x1e\n\x16oemcrypto_core_message\x18\t \x01(\x0c\"\xec\x01\n\x0bMessageType\x12\x13\n\x0fLICENSE_REQUEST\x10\x01\x12\x0b\n\x07LICENSE\x10\x02\x12\x12\n\x0e\x45RROR_RESPONSE\x10\x03\x12\x1f\n\x1bSERVICE_CERTIFICATE_REQUEST\x10\x04\x12\x17\n\x13SERVICE_CERTIFICATE\x10\x05\x12\x0f\n\x0bSUB_LICENSE\x10\x06\x12\x17\n\x13\x43\x41S_LICENSE_REQUEST\x10\x07\x12\x0f\n\x0b\x43\x41S_LICENSE\x10\x08\x12\x1c\n\x18\x45XTERNAL_LICENSE_REQUEST\x10\t\x12\x14\n\x10\x45XTERNAL_LICENSE\x10\n\"S\n\x0eSessionKeyType\x12\r\n\tUNDEFINED\x10\x00\x12\x13\n\x0fWRAPPED_AES_KEY\x10\x01\x12\x1d\n\x19\x45PHERMERAL_ECC_PUBLIC_KEY\x10\x02\"\xc7\x0e\n\x14\x43lientIdentification\x12Q\n\x04type\x18\x01 \x01(\x0e\x32;.pywidevine_license_protocol.ClientIdentification.TokenType:\x06KEYBOX\x12\r\n\x05token\x18\x02 \x01(\x0c\x12P\n\x0b\x63lient_info\x18\x03 \x03(\x0b\x32;.pywidevine_license_protocol.ClientIdentification.NameValue\x12\x1d\n\x15provider_client_token\x18\x04 \x01(\x0c\x12\x17\n\x0flicense_counter\x18\x05 \x01(\r\x12\x61\n\x13\x63lient_capabilities\x18\x06 \x01(\x0b\x32\x44.pywidevine_license_protocol.ClientIdentification.ClientCapabilities\x12\x10\n\x08vmp_data\x18\x07 \x01(\x0c\x12_\n\x12\x64\x65vice_credentials\x18\x08 \x03(\x0b\x32\x43.pywidevine_license_protocol.ClientIdentification.ClientCredentials\x1a(\n\tNameValue\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\x1a\xd6\x08\n\x12\x43lientCapabilities\x12\x1b\n\x0c\x63lient_token\x18\x01 \x01(\x08:\x05\x66\x61lse\x12\x1c\n\rsession_token\x18\x02 \x01(\x08:\x05\x66\x61lse\x12+\n\x1cvideo_resolution_constraints\x18\x03 \x01(\x08:\x05\x66\x61lse\x12u\n\x10max_hdcp_version\x18\x04 \x01(\x0e\x32P.pywidevine_license_protocol.ClientIdentification.ClientCapabilities.HdcpVersion:\tHDCP_NONE\x12\x1e\n\x16oem_crypto_api_version\x18\x05 \x01(\r\x12(\n\x19\x61nti_rollback_usage_table\x18\x06 \x01(\x08:\x05\x66\x61lse\x12\x13\n\x0bsrm_version\x18\x07 \x01(\r\x12\x1d\n\x0e\x63\x61n_update_srm\x18\x08 \x01(\x08:\x05\x66\x61lse\x12\x7f\n\x1esupported_certificate_key_type\x18\t \x03(\x0e\x32W.pywidevine_license_protocol.ClientIdentification.ClientCapabilities.CertificateKeyType\x12\x98\x01\n\x1a\x61nalog_output_capabilities\x18\n \x01(\x0e\x32].pywidevine_license_protocol.ClientIdentification.ClientCapabilities.AnalogOutputCapabilities:\x15\x41NALOG_OUTPUT_UNKNOWN\x12(\n\x19\x63\x61n_disable_analog_output\x18\x0b \x01(\x08:\x05\x66\x61lse\x12\x1f\n\x14resource_rating_tier\x18\x0c \x01(\r:\x01\x30\"\x80\x01\n\x0bHdcpVersion\x12\r\n\tHDCP_NONE\x10\x00\x12\x0b\n\x07HDCP_V1\x10\x01\x12\x0b\n\x07HDCP_V2\x10\x02\x12\r\n\tHDCP_V2_1\x10\x03\x12\r\n\tHDCP_V2_2\x10\x04\x12\r\n\tHDCP_V2_3\x10\x05\x12\x1b\n\x16HDCP_NO_DIGITAL_OUTPUT\x10\xff\x01\"i\n\x12\x43\x65rtificateKeyType\x12\x0c\n\x08RSA_2048\x10\x00\x12\x0c\n\x08RSA_3072\x10\x01\x12\x11\n\rECC_SECP256R1\x10\x02\x12\x11\n\rECC_SECP384R1\x10\x03\x12\x11\n\rECC_SECP521R1\x10\x04\"\x8d\x01\n\x18\x41nalogOutputCapabilities\x12\x19\n\x15\x41NALOG_OUTPUT_UNKNOWN\x10\x00\x12\x16\n\x12\x41NALOG_OUTPUT_NONE\x10\x01\x12\x1b\n\x17\x41NALOG_OUTPUT_SUPPORTED\x10\x02\x12!\n\x1d\x41NALOG_OUTPUT_SUPPORTS_CGMS_A\x10\x03\x1au\n\x11\x43lientCredentials\x12Q\n\x04type\x18\x01 \x01(\x0e\x32;.pywidevine_license_protocol.ClientIdentification.TokenType:\x06KEYBOX\x12\r\n\x05token\x18\x02 \x01(\x0c\"s\n\tTokenType\x12\n\n\x06KEYBOX\x10\x00\x12\x1a\n\x16\x44RM_DEVICE_CERTIFICATE\x10\x01\x12\"\n\x1eREMOTE_ATTESTATION_CERTIFICATE\x10\x02\x12\x1a\n\x16OEM_DEVICE_CERTIFICATE\x10\x03\"\xbb\x01\n\x1d\x45ncryptedClientIdentification\x12\x13\n\x0bprovider_id\x18\x01 \x01(\t\x12)\n!service_certificate_serial_number\x18\x02 \x01(\x0c\x12\x1b\n\x13\x65ncrypted_client_id\x18\x03 \x01(\x0c\x12\x1e\n\x16\x65ncrypted_client_id_iv\x18\x04 \x01(\x0c\x12\x1d\n\x15\x65ncrypted_privacy_key\x18\x05 \x01(\x0c\"\xba\x07\n\x0e\x44rmCertificate\x12>\n\x04type\x18\x01 \x01(\x0e\x32\x30.pywidevine_license_protocol.DrmCertificate.Type\x12\x15\n\rserial_number\x18\x02 \x01(\x0c\x12\x1d\n\x15\x63reation_time_seconds\x18\x03 \x01(\r\x12\x1f\n\x17\x65xpiration_time_seconds\x18\x0c \x01(\r\x12\x12\n\npublic_key\x18\x04 \x01(\x0c\x12\x11\n\tsystem_id\x18\x05 \x01(\r\x12\"\n\x16test_device_deprecated\x18\x06 \x01(\x08\x42\x02\x18\x01\x12\x13\n\x0bprovider_id\x18\x07 \x01(\t\x12N\n\rservice_types\x18\x08 \x03(\x0e\x32\x37.pywidevine_license_protocol.DrmCertificate.ServiceType\x12M\n\talgorithm\x18\t \x01(\x0e\x32\x35.pywidevine_license_protocol.DrmCertificate.Algorithm:\x03RSA\x12\x0e\n\x06rot_id\x18\n \x01(\x0c\x12Q\n\x0e\x65ncryption_key\x18\x0b \x01(\x0b\x32\x39.pywidevine_license_protocol.DrmCertificate.EncryptionKey\x1ar\n\rEncryptionKey\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12M\n\talgorithm\x18\x02 \x01(\x0e\x32\x35.pywidevine_license_protocol.DrmCertificate.Algorithm:\x03RSA\"L\n\x04Type\x12\x08\n\x04ROOT\x10\x00\x12\x10\n\x0c\x44\x45VICE_MODEL\x10\x01\x12\n\n\x06\x44\x45VICE\x10\x02\x12\x0b\n\x07SERVICE\x10\x03\x12\x0f\n\x0bPROVISIONER\x10\x04\"\x86\x01\n\x0bServiceType\x12\x18\n\x14UNKNOWN_SERVICE_TYPE\x10\x00\x12\x16\n\x12LICENSE_SERVER_SDK\x10\x01\x12\x1c\n\x18LICENSE_SERVER_PROXY_SDK\x10\x02\x12\x14\n\x10PROVISIONING_SDK\x10\x03\x12\x11\n\rCAS_PROXY_SDK\x10\x04\"d\n\tAlgorithm\x12\x15\n\x11UNKNOWN_ALGORITHM\x10\x00\x12\x07\n\x03RSA\x10\x01\x12\x11\n\rECC_SECP256R1\x10\x02\x12\x11\n\rECC_SECP384R1\x10\x03\x12\x11\n\rECC_SECP521R1\x10\x04\"\xce\x01\n\x14SignedDrmCertificate\x12\x17\n\x0f\x64rm_certificate\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x41\n\x06signer\x18\x03 \x01(\x0b\x32\x31.pywidevine_license_protocol.SignedDrmCertificate\x12G\n\x0ehash_algorithm\x18\x04 \x01(\x0e\x32/.pywidevine_license_protocol.HashAlgorithmProto\"\xf6\x05\n\x10WidevinePsshData\x12\x0f\n\x07key_ids\x18\x02 \x03(\x0c\x12\x12\n\ncontent_id\x18\x04 \x01(\x0c\x12\x1b\n\x13\x63rypto_period_index\x18\x07 \x01(\r\x12\x19\n\x11protection_scheme\x18\t \x01(\r\x12\x1d\n\x15\x63rypto_period_seconds\x18\n \x01(\r\x12H\n\x04type\x18\x0b \x01(\x0e\x32\x32.pywidevine_license_protocol.WidevinePsshData.Type:\x06SINGLE\x12\x14\n\x0ckey_sequence\x18\x0c \x01(\r\x12\x11\n\tgroup_ids\x18\r \x03(\x0c\x12P\n\rentitled_keys\x18\x0e \x03(\x0b\x32\x39.pywidevine_license_protocol.WidevinePsshData.EntitledKey\x12\x15\n\rvideo_feature\x18\x0f \x01(\t\x12N\n\talgorithm\x18\x01 \x01(\x0e\x32\x37.pywidevine_license_protocol.WidevinePsshData.AlgorithmB\x02\x18\x01\x12\x14\n\x08provider\x18\x03 \x01(\tB\x02\x18\x01\x12\x16\n\ntrack_type\x18\x05 \x01(\tB\x02\x18\x01\x12\x12\n\x06policy\x18\x06 \x01(\tB\x02\x18\x01\x12\x1b\n\x0fgrouped_license\x18\x08 \x01(\x0c\x42\x02\x18\x01\x1az\n\x0b\x45ntitledKey\x12\x1a\n\x12\x65ntitlement_key_id\x18\x01 \x01(\x0c\x12\x0e\n\x06key_id\x18\x02 \x01(\x0c\x12\x0b\n\x03key\x18\x03 \x01(\x0c\x12\n\n\x02iv\x18\x04 \x01(\x0c\x12&\n\x1a\x65ntitlement_key_size_bytes\x18\x05 \x01(\r:\x02\x33\x32\"5\n\x04Type\x12\n\n\x06SINGLE\x10\x00\x12\x0f\n\x0b\x45NTITLEMENT\x10\x01\x12\x10\n\x0c\x45NTITLED_KEY\x10\x02\"(\n\tAlgorithm\x12\x0f\n\x0bUNENCRYPTED\x10\x00\x12\n\n\x06\x41\x45SCTR\x10\x01\"\xd1\x01\n\nFileHashes\x12\x0e\n\x06signer\x18\x01 \x01(\x0c\x12\x45\n\nsignatures\x18\x02 \x03(\x0b\x32\x31.pywidevine_license_protocol.FileHashes.Signature\x1al\n\tSignature\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x14\n\x0ctest_signing\x18\x02 \x01(\x08\x12\x12\n\nSHA512Hash\x18\x03 \x01(\x0c\x12\x10\n\x08main_exe\x18\x04 \x01(\x08\x12\x11\n\tsignature\x18\x05 \x01(\x0c*8\n\x0bLicenseType\x12\r\n\tSTREAMING\x10\x01\x12\x0b\n\x07OFFLINE\x10\x02\x12\r\n\tAUTOMATIC\x10\x03*\xd9\x01\n\x1aPlatformVerificationStatus\x12\x17\n\x13PLATFORM_UNVERIFIED\x10\x00\x12\x15\n\x11PLATFORM_TAMPERED\x10\x01\x12\x1e\n\x1aPLATFORM_SOFTWARE_VERIFIED\x10\x02\x12\x1e\n\x1aPLATFORM_HARDWARE_VERIFIED\x10\x03\x12\x1c\n\x18PLATFORM_NO_VERIFICATION\x10\x04\x12-\n)PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED\x10\x05*D\n\x0fProtocolVersion\x12\x0f\n\x0bVERSION_2_0\x10\x14\x12\x0f\n\x0bVERSION_2_1\x10\x15\x12\x0f\n\x0bVERSION_2_2\x10\x16*\x86\x01\n\x12HashAlgorithmProto\x12\x1e\n\x1aHASH_ALGORITHM_UNSPECIFIED\x10\x00\x12\x18\n\x14HASH_ALGORITHM_SHA_1\x10\x01\x12\x1a\n\x16HASH_ALGORITHM_SHA_256\x10\x02\x12\x1a\n\x16HASH_ALGORITHM_SHA_384\x10\x03\x42$\n com.rlaphoenix.pywidevine.protosH\x03')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'license_protocol_pb2', _globals)
+if _descriptor._USE_C_DESCRIPTORS == False:
+ _globals['DESCRIPTOR']._options = None
+ _globals['DESCRIPTOR']._serialized_options = b'\n com.rlaphoenix.pywidevine.protosH\003'
+ _globals['_DRMCERTIFICATE'].fields_by_name['test_device_deprecated']._options = None
+ _globals['_DRMCERTIFICATE'].fields_by_name['test_device_deprecated']._serialized_options = b'\030\001'
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['algorithm']._options = None
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['algorithm']._serialized_options = b'\030\001'
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['provider']._options = None
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['provider']._serialized_options = b'\030\001'
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['track_type']._options = None
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['track_type']._serialized_options = b'\030\001'
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['policy']._options = None
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['policy']._serialized_options = b'\030\001'
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['grouped_license']._options = None
+ _globals['_WIDEVINEPSSHDATA'].fields_by_name['grouped_license']._serialized_options = b'\030\001'
+ _globals['_LICENSETYPE']._serialized_start=10442
+ _globals['_LICENSETYPE']._serialized_end=10498
+ _globals['_PLATFORMVERIFICATIONSTATUS']._serialized_start=10501
+ _globals['_PLATFORMVERIFICATIONSTATUS']._serialized_end=10718
+ _globals['_PROTOCOLVERSION']._serialized_start=10720
+ _globals['_PROTOCOLVERSION']._serialized_end=10788
+ _globals['_HASHALGORITHMPROTO']._serialized_start=10791
+ _globals['_HASHALGORITHMPROTO']._serialized_end=10925
+ _globals['_LICENSEIDENTIFICATION']._serialized_start=56
+ _globals['_LICENSEIDENTIFICATION']._serialized_end=245
+ _globals['_LICENSE']._serialized_start=248
+ _globals['_LICENSE']._serialized_end=3433
+ _globals['_LICENSE_POLICY']._serialized_start=764
+ _globals['_LICENSE_POLICY']._serialized_end=1322
+ _globals['_LICENSE_KEYCONTAINER']._serialized_start=1325
+ _globals['_LICENSE_KEYCONTAINER']._serialized_end=3433
+ _globals['_LICENSE_KEYCONTAINER_KEYCONTROL']._serialized_start=2128
+ _globals['_LICENSE_KEYCONTAINER_KEYCONTROL']._serialized_end=2179
+ _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION']._serialized_start=2182
+ _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION']._serialized_end=2850
+ _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_HDCP']._serialized_start=2604
+ _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_HDCP']._serialized_end=2725
+ _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS']._serialized_start=2727
+ _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_CGMS']._serialized_end=2794
+ _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_HDCPSRMRULE']._serialized_start=2796
+ _globals['_LICENSE_KEYCONTAINER_OUTPUTPROTECTION_HDCPSRMRULE']._serialized_end=2850
+ _globals['_LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT']._serialized_start=2853
+ _globals['_LICENSE_KEYCONTAINER_VIDEORESOLUTIONCONSTRAINT']._serialized_end=3039
+ _globals['_LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS']._serialized_start=3042
+ _globals['_LICENSE_KEYCONTAINER_OPERATORSESSIONKEYPERMISSIONS']._serialized_end=3199
+ _globals['_LICENSE_KEYCONTAINER_KEYTYPE']._serialized_start=3201
+ _globals['_LICENSE_KEYCONTAINER_KEYTYPE']._serialized_end=3309
+ _globals['_LICENSE_KEYCONTAINER_SECURITYLEVEL']._serialized_start=3311
+ _globals['_LICENSE_KEYCONTAINER_SECURITYLEVEL']._serialized_end=3433
+ _globals['_LICENSEREQUEST']._serialized_start=3436
+ _globals['_LICENSEREQUEST']._serialized_end=5161
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION']._serialized_start=3944
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION']._serialized_end=5111
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_WIDEVINEPSSHDATA']._serialized_start=4391
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_WIDEVINEPSSHDATA']._serialized_end=4512
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_WEBMKEYID']._serialized_start=4514
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_WEBMKEYID']._serialized_end=4625
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE']._serialized_start=4628
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_EXISTINGLICENSE']._serialized_end=4818
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA']._serialized_start=4821
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA']._serialized_end=5089
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA_INITDATATYPE']._serialized_start=5055
+ _globals['_LICENSEREQUEST_CONTENTIDENTIFICATION_INITDATA_INITDATATYPE']._serialized_end=5089
+ _globals['_LICENSEREQUEST_REQUESTTYPE']._serialized_start=5113
+ _globals['_LICENSEREQUEST_REQUESTTYPE']._serialized_end=5161
+ _globals['_METRICDATA']._serialized_start=5164
+ _globals['_METRICDATA']._serialized_end=5407
+ _globals['_METRICDATA_TYPEVALUE']._serialized_start=5270
+ _globals['_METRICDATA_TYPEVALUE']._serialized_end=5365
+ _globals['_METRICDATA_METRICTYPE']._serialized_start=5367
+ _globals['_METRICDATA_METRICTYPE']._serialized_end=5407
+ _globals['_VERSIONINFO']._serialized_start=5409
+ _globals['_VERSIONINFO']._serialized_end=5484
+ _globals['_SIGNEDMESSAGE']._serialized_start=5487
+ _globals['_SIGNEDMESSAGE']._serialized_end=6245
+ _globals['_SIGNEDMESSAGE_MESSAGETYPE']._serialized_start=5924
+ _globals['_SIGNEDMESSAGE_MESSAGETYPE']._serialized_end=6160
+ _globals['_SIGNEDMESSAGE_SESSIONKEYTYPE']._serialized_start=6162
+ _globals['_SIGNEDMESSAGE_SESSIONKEYTYPE']._serialized_end=6245
+ _globals['_CLIENTIDENTIFICATION']._serialized_start=6248
+ _globals['_CLIENTIDENTIFICATION']._serialized_end=8111
+ _globals['_CLIENTIDENTIFICATION_NAMEVALUE']._serialized_start=6722
+ _globals['_CLIENTIDENTIFICATION_NAMEVALUE']._serialized_end=6762
+ _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES']._serialized_start=6765
+ _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES']._serialized_end=7875
+ _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION']._serialized_start=7496
+ _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_HDCPVERSION']._serialized_end=7624
+ _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE']._serialized_start=7626
+ _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_CERTIFICATEKEYTYPE']._serialized_end=7731
+ _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_ANALOGOUTPUTCAPABILITIES']._serialized_start=7734
+ _globals['_CLIENTIDENTIFICATION_CLIENTCAPABILITIES_ANALOGOUTPUTCAPABILITIES']._serialized_end=7875
+ _globals['_CLIENTIDENTIFICATION_CLIENTCREDENTIALS']._serialized_start=7877
+ _globals['_CLIENTIDENTIFICATION_CLIENTCREDENTIALS']._serialized_end=7994
+ _globals['_CLIENTIDENTIFICATION_TOKENTYPE']._serialized_start=7996
+ _globals['_CLIENTIDENTIFICATION_TOKENTYPE']._serialized_end=8111
+ _globals['_ENCRYPTEDCLIENTIDENTIFICATION']._serialized_start=8114
+ _globals['_ENCRYPTEDCLIENTIDENTIFICATION']._serialized_end=8301
+ _globals['_DRMCERTIFICATE']._serialized_start=8304
+ _globals['_DRMCERTIFICATE']._serialized_end=9258
+ _globals['_DRMCERTIFICATE_ENCRYPTIONKEY']._serialized_start=8827
+ _globals['_DRMCERTIFICATE_ENCRYPTIONKEY']._serialized_end=8941
+ _globals['_DRMCERTIFICATE_TYPE']._serialized_start=8943
+ _globals['_DRMCERTIFICATE_TYPE']._serialized_end=9019
+ _globals['_DRMCERTIFICATE_SERVICETYPE']._serialized_start=9022
+ _globals['_DRMCERTIFICATE_SERVICETYPE']._serialized_end=9156
+ _globals['_DRMCERTIFICATE_ALGORITHM']._serialized_start=9158
+ _globals['_DRMCERTIFICATE_ALGORITHM']._serialized_end=9258
+ _globals['_SIGNEDDRMCERTIFICATE']._serialized_start=9261
+ _globals['_SIGNEDDRMCERTIFICATE']._serialized_end=9467
+ _globals['_WIDEVINEPSSHDATA']._serialized_start=9470
+ _globals['_WIDEVINEPSSHDATA']._serialized_end=10228
+ _globals['_WIDEVINEPSSHDATA_ENTITLEDKEY']._serialized_start=10009
+ _globals['_WIDEVINEPSSHDATA_ENTITLEDKEY']._serialized_end=10131
+ _globals['_WIDEVINEPSSHDATA_TYPE']._serialized_start=10133
+ _globals['_WIDEVINEPSSHDATA_TYPE']._serialized_end=10186
+ _globals['_WIDEVINEPSSHDATA_ALGORITHM']._serialized_start=10188
+ _globals['_WIDEVINEPSSHDATA_ALGORITHM']._serialized_end=10228
+ _globals['_FILEHASHES']._serialized_start=10231
+ _globals['_FILEHASHES']._serialized_end=10440
+ _globals['_FILEHASHES_SIGNATURE']._serialized_start=10332
+ _globals['_FILEHASHES_SIGNATURE']._serialized_end=10440
+# @@protoc_insertion_point(module_scope)
diff --git a/pywidevine/license_protocol_pb2.pyi b/pywidevine/license_protocol_pb2.pyi
new file mode 100644
index 0000000..44f543e
--- /dev/null
+++ b/pywidevine/license_protocol_pb2.pyi
@@ -0,0 +1,607 @@
+# mypy: ignore-errors
+
+from google.protobuf.internal import containers as _containers
+from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
+
+AUTOMATIC: LicenseType
+DESCRIPTOR: _descriptor.FileDescriptor
+HASH_ALGORITHM_SHA_1: HashAlgorithmProto
+HASH_ALGORITHM_SHA_256: HashAlgorithmProto
+HASH_ALGORITHM_SHA_384: HashAlgorithmProto
+HASH_ALGORITHM_UNSPECIFIED: HashAlgorithmProto
+OFFLINE: LicenseType
+PLATFORM_HARDWARE_VERIFIED: PlatformVerificationStatus
+PLATFORM_NO_VERIFICATION: PlatformVerificationStatus
+PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED: PlatformVerificationStatus
+PLATFORM_SOFTWARE_VERIFIED: PlatformVerificationStatus
+PLATFORM_TAMPERED: PlatformVerificationStatus
+PLATFORM_UNVERIFIED: PlatformVerificationStatus
+STREAMING: LicenseType
+VERSION_2_0: ProtocolVersion
+VERSION_2_1: ProtocolVersion
+VERSION_2_2: ProtocolVersion
+
+class ClientIdentification(_message.Message):
+ __slots__ = ["client_capabilities", "client_info", "device_credentials", "license_counter", "provider_client_token", "token", "type", "vmp_data"]
+ class TokenType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class ClientCapabilities(_message.Message):
+ __slots__ = ["analog_output_capabilities", "anti_rollback_usage_table", "can_disable_analog_output", "can_update_srm", "client_token", "max_hdcp_version", "oem_crypto_api_version", "resource_rating_tier", "session_token", "srm_version", "supported_certificate_key_type", "video_resolution_constraints"]
+ class AnalogOutputCapabilities(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class CertificateKeyType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class HdcpVersion(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ ANALOG_OUTPUT_CAPABILITIES_FIELD_NUMBER: _ClassVar[int]
+ ANALOG_OUTPUT_NONE: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities
+ ANALOG_OUTPUT_SUPPORTED: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities
+ ANALOG_OUTPUT_SUPPORTS_CGMS_A: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities
+ ANALOG_OUTPUT_UNKNOWN: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities
+ ANTI_ROLLBACK_USAGE_TABLE_FIELD_NUMBER: _ClassVar[int]
+ CAN_DISABLE_ANALOG_OUTPUT_FIELD_NUMBER: _ClassVar[int]
+ CAN_UPDATE_SRM_FIELD_NUMBER: _ClassVar[int]
+ CLIENT_TOKEN_FIELD_NUMBER: _ClassVar[int]
+ ECC_SECP256R1: ClientIdentification.ClientCapabilities.CertificateKeyType
+ ECC_SECP384R1: ClientIdentification.ClientCapabilities.CertificateKeyType
+ ECC_SECP521R1: ClientIdentification.ClientCapabilities.CertificateKeyType
+ HDCP_NONE: ClientIdentification.ClientCapabilities.HdcpVersion
+ HDCP_NO_DIGITAL_OUTPUT: ClientIdentification.ClientCapabilities.HdcpVersion
+ HDCP_V1: ClientIdentification.ClientCapabilities.HdcpVersion
+ HDCP_V2: ClientIdentification.ClientCapabilities.HdcpVersion
+ HDCP_V2_1: ClientIdentification.ClientCapabilities.HdcpVersion
+ HDCP_V2_2: ClientIdentification.ClientCapabilities.HdcpVersion
+ HDCP_V2_3: ClientIdentification.ClientCapabilities.HdcpVersion
+ MAX_HDCP_VERSION_FIELD_NUMBER: _ClassVar[int]
+ OEM_CRYPTO_API_VERSION_FIELD_NUMBER: _ClassVar[int]
+ RESOURCE_RATING_TIER_FIELD_NUMBER: _ClassVar[int]
+ RSA_2048: ClientIdentification.ClientCapabilities.CertificateKeyType
+ RSA_3072: ClientIdentification.ClientCapabilities.CertificateKeyType
+ SESSION_TOKEN_FIELD_NUMBER: _ClassVar[int]
+ SRM_VERSION_FIELD_NUMBER: _ClassVar[int]
+ SUPPORTED_CERTIFICATE_KEY_TYPE_FIELD_NUMBER: _ClassVar[int]
+ VIDEO_RESOLUTION_CONSTRAINTS_FIELD_NUMBER: _ClassVar[int]
+ analog_output_capabilities: ClientIdentification.ClientCapabilities.AnalogOutputCapabilities
+ anti_rollback_usage_table: bool
+ can_disable_analog_output: bool
+ can_update_srm: bool
+ client_token: bool
+ max_hdcp_version: ClientIdentification.ClientCapabilities.HdcpVersion
+ oem_crypto_api_version: int
+ resource_rating_tier: int
+ session_token: bool
+ srm_version: int
+ supported_certificate_key_type: _containers.RepeatedScalarFieldContainer[ClientIdentification.ClientCapabilities.CertificateKeyType]
+ video_resolution_constraints: bool
+ def __init__(self, client_token: bool = ..., session_token: bool = ..., video_resolution_constraints: bool = ..., max_hdcp_version: _Optional[_Union[ClientIdentification.ClientCapabilities.HdcpVersion, str]] = ..., oem_crypto_api_version: _Optional[int] = ..., anti_rollback_usage_table: bool = ..., srm_version: _Optional[int] = ..., can_update_srm: bool = ..., supported_certificate_key_type: _Optional[_Iterable[_Union[ClientIdentification.ClientCapabilities.CertificateKeyType, str]]] = ..., analog_output_capabilities: _Optional[_Union[ClientIdentification.ClientCapabilities.AnalogOutputCapabilities, str]] = ..., can_disable_analog_output: bool = ..., resource_rating_tier: _Optional[int] = ...) -> None: ...
+ class ClientCredentials(_message.Message):
+ __slots__ = ["token", "type"]
+ TOKEN_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ token: bytes
+ type: ClientIdentification.TokenType
+ def __init__(self, type: _Optional[_Union[ClientIdentification.TokenType, str]] = ..., token: _Optional[bytes] = ...) -> None: ...
+ class NameValue(_message.Message):
+ __slots__ = ["name", "value"]
+ NAME_FIELD_NUMBER: _ClassVar[int]
+ VALUE_FIELD_NUMBER: _ClassVar[int]
+ name: str
+ value: str
+ def __init__(self, name: _Optional[str] = ..., value: _Optional[str] = ...) -> None: ...
+ CLIENT_CAPABILITIES_FIELD_NUMBER: _ClassVar[int]
+ CLIENT_INFO_FIELD_NUMBER: _ClassVar[int]
+ DEVICE_CREDENTIALS_FIELD_NUMBER: _ClassVar[int]
+ DRM_DEVICE_CERTIFICATE: ClientIdentification.TokenType
+ KEYBOX: ClientIdentification.TokenType
+ LICENSE_COUNTER_FIELD_NUMBER: _ClassVar[int]
+ OEM_DEVICE_CERTIFICATE: ClientIdentification.TokenType
+ PROVIDER_CLIENT_TOKEN_FIELD_NUMBER: _ClassVar[int]
+ REMOTE_ATTESTATION_CERTIFICATE: ClientIdentification.TokenType
+ TOKEN_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ VMP_DATA_FIELD_NUMBER: _ClassVar[int]
+ client_capabilities: ClientIdentification.ClientCapabilities
+ client_info: _containers.RepeatedCompositeFieldContainer[ClientIdentification.NameValue]
+ device_credentials: _containers.RepeatedCompositeFieldContainer[ClientIdentification.ClientCredentials]
+ license_counter: int
+ provider_client_token: bytes
+ token: bytes
+ type: ClientIdentification.TokenType
+ vmp_data: bytes
+ def __init__(self, type: _Optional[_Union[ClientIdentification.TokenType, str]] = ..., token: _Optional[bytes] = ..., client_info: _Optional[_Iterable[_Union[ClientIdentification.NameValue, _Mapping]]] = ..., provider_client_token: _Optional[bytes] = ..., license_counter: _Optional[int] = ..., client_capabilities: _Optional[_Union[ClientIdentification.ClientCapabilities, _Mapping]] = ..., vmp_data: _Optional[bytes] = ..., device_credentials: _Optional[_Iterable[_Union[ClientIdentification.ClientCredentials, _Mapping]]] = ...) -> None: ...
+
+class DrmCertificate(_message.Message):
+ __slots__ = ["algorithm", "creation_time_seconds", "encryption_key", "expiration_time_seconds", "provider_id", "public_key", "rot_id", "serial_number", "service_types", "system_id", "test_device_deprecated", "type"]
+ class Algorithm(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class ServiceType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class Type(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class EncryptionKey(_message.Message):
+ __slots__ = ["algorithm", "public_key"]
+ ALGORITHM_FIELD_NUMBER: _ClassVar[int]
+ PUBLIC_KEY_FIELD_NUMBER: _ClassVar[int]
+ algorithm: DrmCertificate.Algorithm
+ public_key: bytes
+ def __init__(self, public_key: _Optional[bytes] = ..., algorithm: _Optional[_Union[DrmCertificate.Algorithm, str]] = ...) -> None: ...
+ ALGORITHM_FIELD_NUMBER: _ClassVar[int]
+ CAS_PROXY_SDK: DrmCertificate.ServiceType
+ CREATION_TIME_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ DEVICE: DrmCertificate.Type
+ DEVICE_MODEL: DrmCertificate.Type
+ ECC_SECP256R1: DrmCertificate.Algorithm
+ ECC_SECP384R1: DrmCertificate.Algorithm
+ ECC_SECP521R1: DrmCertificate.Algorithm
+ ENCRYPTION_KEY_FIELD_NUMBER: _ClassVar[int]
+ EXPIRATION_TIME_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ LICENSE_SERVER_PROXY_SDK: DrmCertificate.ServiceType
+ LICENSE_SERVER_SDK: DrmCertificate.ServiceType
+ PROVIDER_ID_FIELD_NUMBER: _ClassVar[int]
+ PROVISIONER: DrmCertificate.Type
+ PROVISIONING_SDK: DrmCertificate.ServiceType
+ PUBLIC_KEY_FIELD_NUMBER: _ClassVar[int]
+ ROOT: DrmCertificate.Type
+ ROT_ID_FIELD_NUMBER: _ClassVar[int]
+ RSA: DrmCertificate.Algorithm
+ SERIAL_NUMBER_FIELD_NUMBER: _ClassVar[int]
+ SERVICE: DrmCertificate.Type
+ SERVICE_TYPES_FIELD_NUMBER: _ClassVar[int]
+ SYSTEM_ID_FIELD_NUMBER: _ClassVar[int]
+ TEST_DEVICE_DEPRECATED_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ UNKNOWN_ALGORITHM: DrmCertificate.Algorithm
+ UNKNOWN_SERVICE_TYPE: DrmCertificate.ServiceType
+ algorithm: DrmCertificate.Algorithm
+ creation_time_seconds: int
+ encryption_key: DrmCertificate.EncryptionKey
+ expiration_time_seconds: int
+ provider_id: str
+ public_key: bytes
+ rot_id: bytes
+ serial_number: bytes
+ service_types: _containers.RepeatedScalarFieldContainer[DrmCertificate.ServiceType]
+ system_id: int
+ test_device_deprecated: bool
+ type: DrmCertificate.Type
+ def __init__(self, type: _Optional[_Union[DrmCertificate.Type, str]] = ..., serial_number: _Optional[bytes] = ..., creation_time_seconds: _Optional[int] = ..., expiration_time_seconds: _Optional[int] = ..., public_key: _Optional[bytes] = ..., system_id: _Optional[int] = ..., test_device_deprecated: bool = ..., provider_id: _Optional[str] = ..., service_types: _Optional[_Iterable[_Union[DrmCertificate.ServiceType, str]]] = ..., algorithm: _Optional[_Union[DrmCertificate.Algorithm, str]] = ..., rot_id: _Optional[bytes] = ..., encryption_key: _Optional[_Union[DrmCertificate.EncryptionKey, _Mapping]] = ...) -> None: ...
+
+class EncryptedClientIdentification(_message.Message):
+ __slots__ = ["encrypted_client_id", "encrypted_client_id_iv", "encrypted_privacy_key", "provider_id", "service_certificate_serial_number"]
+ ENCRYPTED_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
+ ENCRYPTED_CLIENT_ID_IV_FIELD_NUMBER: _ClassVar[int]
+ ENCRYPTED_PRIVACY_KEY_FIELD_NUMBER: _ClassVar[int]
+ PROVIDER_ID_FIELD_NUMBER: _ClassVar[int]
+ SERVICE_CERTIFICATE_SERIAL_NUMBER_FIELD_NUMBER: _ClassVar[int]
+ encrypted_client_id: bytes
+ encrypted_client_id_iv: bytes
+ encrypted_privacy_key: bytes
+ provider_id: str
+ service_certificate_serial_number: bytes
+ def __init__(self, provider_id: _Optional[str] = ..., service_certificate_serial_number: _Optional[bytes] = ..., encrypted_client_id: _Optional[bytes] = ..., encrypted_client_id_iv: _Optional[bytes] = ..., encrypted_privacy_key: _Optional[bytes] = ...) -> None: ...
+
+class FileHashes(_message.Message):
+ __slots__ = ["signatures", "signer"]
+ class Signature(_message.Message):
+ __slots__ = ["SHA512Hash", "filename", "main_exe", "signature", "test_signing"]
+ FILENAME_FIELD_NUMBER: _ClassVar[int]
+ MAIN_EXE_FIELD_NUMBER: _ClassVar[int]
+ SHA512HASH_FIELD_NUMBER: _ClassVar[int]
+ SHA512Hash: bytes
+ SIGNATURE_FIELD_NUMBER: _ClassVar[int]
+ TEST_SIGNING_FIELD_NUMBER: _ClassVar[int]
+ filename: str
+ main_exe: bool
+ signature: bytes
+ test_signing: bool
+ def __init__(self, filename: _Optional[str] = ..., test_signing: bool = ..., SHA512Hash: _Optional[bytes] = ..., main_exe: bool = ..., signature: _Optional[bytes] = ...) -> None: ...
+ SIGNATURES_FIELD_NUMBER: _ClassVar[int]
+ SIGNER_FIELD_NUMBER: _ClassVar[int]
+ signatures: _containers.RepeatedCompositeFieldContainer[FileHashes.Signature]
+ signer: bytes
+ def __init__(self, signer: _Optional[bytes] = ..., signatures: _Optional[_Iterable[_Union[FileHashes.Signature, _Mapping]]] = ...) -> None: ...
+
+class License(_message.Message):
+ __slots__ = ["group_ids", "id", "key", "license_start_time", "platform_verification_status", "policy", "protection_scheme", "provider_client_token", "remote_attestation_verified", "srm_requirement", "srm_update"]
+ class KeyContainer(_message.Message):
+ __slots__ = ["anti_rollback_usage_table", "id", "iv", "key", "key_control", "level", "operator_session_key_permissions", "requested_protection", "required_protection", "track_label", "type", "video_resolution_constraints"]
+ class KeyType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class SecurityLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class KeyControl(_message.Message):
+ __slots__ = ["iv", "key_control_block"]
+ IV_FIELD_NUMBER: _ClassVar[int]
+ KEY_CONTROL_BLOCK_FIELD_NUMBER: _ClassVar[int]
+ iv: bytes
+ key_control_block: bytes
+ def __init__(self, key_control_block: _Optional[bytes] = ..., iv: _Optional[bytes] = ...) -> None: ...
+ class OperatorSessionKeyPermissions(_message.Message):
+ __slots__ = ["allow_decrypt", "allow_encrypt", "allow_sign", "allow_signature_verify"]
+ ALLOW_DECRYPT_FIELD_NUMBER: _ClassVar[int]
+ ALLOW_ENCRYPT_FIELD_NUMBER: _ClassVar[int]
+ ALLOW_SIGNATURE_VERIFY_FIELD_NUMBER: _ClassVar[int]
+ ALLOW_SIGN_FIELD_NUMBER: _ClassVar[int]
+ allow_decrypt: bool
+ allow_encrypt: bool
+ allow_sign: bool
+ allow_signature_verify: bool
+ def __init__(self, allow_encrypt: bool = ..., allow_decrypt: bool = ..., allow_sign: bool = ..., allow_signature_verify: bool = ...) -> None: ...
+ class OutputProtection(_message.Message):
+ __slots__ = ["cgms_flags", "disable_analog_output", "disable_digital_output", "hdcp", "hdcp_srm_rule"]
+ class CGMS(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class HDCP(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class HdcpSrmRule(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ CGMS_FLAGS_FIELD_NUMBER: _ClassVar[int]
+ CGMS_NONE: License.KeyContainer.OutputProtection.CGMS
+ COPY_FREE: License.KeyContainer.OutputProtection.CGMS
+ COPY_NEVER: License.KeyContainer.OutputProtection.CGMS
+ COPY_ONCE: License.KeyContainer.OutputProtection.CGMS
+ CURRENT_SRM: License.KeyContainer.OutputProtection.HdcpSrmRule
+ DISABLE_ANALOG_OUTPUT_FIELD_NUMBER: _ClassVar[int]
+ DISABLE_DIGITAL_OUTPUT_FIELD_NUMBER: _ClassVar[int]
+ HDCP_FIELD_NUMBER: _ClassVar[int]
+ HDCP_NONE: License.KeyContainer.OutputProtection.HDCP
+ HDCP_NO_DIGITAL_OUTPUT: License.KeyContainer.OutputProtection.HDCP
+ HDCP_SRM_RULE_FIELD_NUMBER: _ClassVar[int]
+ HDCP_SRM_RULE_NONE: License.KeyContainer.OutputProtection.HdcpSrmRule
+ HDCP_V1: License.KeyContainer.OutputProtection.HDCP
+ HDCP_V2: License.KeyContainer.OutputProtection.HDCP
+ HDCP_V2_1: License.KeyContainer.OutputProtection.HDCP
+ HDCP_V2_2: License.KeyContainer.OutputProtection.HDCP
+ HDCP_V2_3: License.KeyContainer.OutputProtection.HDCP
+ cgms_flags: License.KeyContainer.OutputProtection.CGMS
+ disable_analog_output: bool
+ disable_digital_output: bool
+ hdcp: License.KeyContainer.OutputProtection.HDCP
+ hdcp_srm_rule: License.KeyContainer.OutputProtection.HdcpSrmRule
+ def __init__(self, hdcp: _Optional[_Union[License.KeyContainer.OutputProtection.HDCP, str]] = ..., cgms_flags: _Optional[_Union[License.KeyContainer.OutputProtection.CGMS, str]] = ..., hdcp_srm_rule: _Optional[_Union[License.KeyContainer.OutputProtection.HdcpSrmRule, str]] = ..., disable_analog_output: bool = ..., disable_digital_output: bool = ...) -> None: ...
+ class VideoResolutionConstraint(_message.Message):
+ __slots__ = ["max_resolution_pixels", "min_resolution_pixels", "required_protection"]
+ MAX_RESOLUTION_PIXELS_FIELD_NUMBER: _ClassVar[int]
+ MIN_RESOLUTION_PIXELS_FIELD_NUMBER: _ClassVar[int]
+ REQUIRED_PROTECTION_FIELD_NUMBER: _ClassVar[int]
+ max_resolution_pixels: int
+ min_resolution_pixels: int
+ required_protection: License.KeyContainer.OutputProtection
+ def __init__(self, min_resolution_pixels: _Optional[int] = ..., max_resolution_pixels: _Optional[int] = ..., required_protection: _Optional[_Union[License.KeyContainer.OutputProtection, _Mapping]] = ...) -> None: ...
+ ANTI_ROLLBACK_USAGE_TABLE_FIELD_NUMBER: _ClassVar[int]
+ CONTENT: License.KeyContainer.KeyType
+ ENTITLEMENT: License.KeyContainer.KeyType
+ HW_SECURE_ALL: License.KeyContainer.SecurityLevel
+ HW_SECURE_CRYPTO: License.KeyContainer.SecurityLevel
+ HW_SECURE_DECODE: License.KeyContainer.SecurityLevel
+ ID_FIELD_NUMBER: _ClassVar[int]
+ IV_FIELD_NUMBER: _ClassVar[int]
+ KEY_CONTROL: License.KeyContainer.KeyType
+ KEY_CONTROL_FIELD_NUMBER: _ClassVar[int]
+ KEY_FIELD_NUMBER: _ClassVar[int]
+ LEVEL_FIELD_NUMBER: _ClassVar[int]
+ OEM_CONTENT: License.KeyContainer.KeyType
+ OPERATOR_SESSION: License.KeyContainer.KeyType
+ OPERATOR_SESSION_KEY_PERMISSIONS_FIELD_NUMBER: _ClassVar[int]
+ REQUESTED_PROTECTION_FIELD_NUMBER: _ClassVar[int]
+ REQUIRED_PROTECTION_FIELD_NUMBER: _ClassVar[int]
+ SIGNING: License.KeyContainer.KeyType
+ SW_SECURE_CRYPTO: License.KeyContainer.SecurityLevel
+ SW_SECURE_DECODE: License.KeyContainer.SecurityLevel
+ TRACK_LABEL_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ VIDEO_RESOLUTION_CONSTRAINTS_FIELD_NUMBER: _ClassVar[int]
+ anti_rollback_usage_table: bool
+ id: bytes
+ iv: bytes
+ key: bytes
+ key_control: License.KeyContainer.KeyControl
+ level: License.KeyContainer.SecurityLevel
+ operator_session_key_permissions: License.KeyContainer.OperatorSessionKeyPermissions
+ requested_protection: License.KeyContainer.OutputProtection
+ required_protection: License.KeyContainer.OutputProtection
+ track_label: str
+ type: License.KeyContainer.KeyType
+ video_resolution_constraints: _containers.RepeatedCompositeFieldContainer[License.KeyContainer.VideoResolutionConstraint]
+ def __init__(self, id: _Optional[bytes] = ..., iv: _Optional[bytes] = ..., key: _Optional[bytes] = ..., type: _Optional[_Union[License.KeyContainer.KeyType, str]] = ..., level: _Optional[_Union[License.KeyContainer.SecurityLevel, str]] = ..., required_protection: _Optional[_Union[License.KeyContainer.OutputProtection, _Mapping]] = ..., requested_protection: _Optional[_Union[License.KeyContainer.OutputProtection, _Mapping]] = ..., key_control: _Optional[_Union[License.KeyContainer.KeyControl, _Mapping]] = ..., operator_session_key_permissions: _Optional[_Union[License.KeyContainer.OperatorSessionKeyPermissions, _Mapping]] = ..., video_resolution_constraints: _Optional[_Iterable[_Union[License.KeyContainer.VideoResolutionConstraint, _Mapping]]] = ..., anti_rollback_usage_table: bool = ..., track_label: _Optional[str] = ...) -> None: ...
+ class Policy(_message.Message):
+ __slots__ = ["always_include_client_id", "can_persist", "can_play", "can_renew", "license_duration_seconds", "play_start_grace_period_seconds", "playback_duration_seconds", "renew_with_usage", "renewal_delay_seconds", "renewal_recovery_duration_seconds", "renewal_retry_interval_seconds", "renewal_server_url", "rental_duration_seconds", "soft_enforce_playback_duration", "soft_enforce_rental_duration"]
+ ALWAYS_INCLUDE_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
+ CAN_PERSIST_FIELD_NUMBER: _ClassVar[int]
+ CAN_PLAY_FIELD_NUMBER: _ClassVar[int]
+ CAN_RENEW_FIELD_NUMBER: _ClassVar[int]
+ LICENSE_DURATION_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ PLAYBACK_DURATION_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ PLAY_START_GRACE_PERIOD_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ RENEWAL_DELAY_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ RENEWAL_RECOVERY_DURATION_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ RENEWAL_RETRY_INTERVAL_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ RENEWAL_SERVER_URL_FIELD_NUMBER: _ClassVar[int]
+ RENEW_WITH_USAGE_FIELD_NUMBER: _ClassVar[int]
+ RENTAL_DURATION_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ SOFT_ENFORCE_PLAYBACK_DURATION_FIELD_NUMBER: _ClassVar[int]
+ SOFT_ENFORCE_RENTAL_DURATION_FIELD_NUMBER: _ClassVar[int]
+ always_include_client_id: bool
+ can_persist: bool
+ can_play: bool
+ can_renew: bool
+ license_duration_seconds: int
+ play_start_grace_period_seconds: int
+ playback_duration_seconds: int
+ renew_with_usage: bool
+ renewal_delay_seconds: int
+ renewal_recovery_duration_seconds: int
+ renewal_retry_interval_seconds: int
+ renewal_server_url: str
+ rental_duration_seconds: int
+ soft_enforce_playback_duration: bool
+ soft_enforce_rental_duration: bool
+ def __init__(self, can_play: bool = ..., can_persist: bool = ..., can_renew: bool = ..., rental_duration_seconds: _Optional[int] = ..., playback_duration_seconds: _Optional[int] = ..., license_duration_seconds: _Optional[int] = ..., renewal_recovery_duration_seconds: _Optional[int] = ..., renewal_server_url: _Optional[str] = ..., renewal_delay_seconds: _Optional[int] = ..., renewal_retry_interval_seconds: _Optional[int] = ..., renew_with_usage: bool = ..., always_include_client_id: bool = ..., play_start_grace_period_seconds: _Optional[int] = ..., soft_enforce_playback_duration: bool = ..., soft_enforce_rental_duration: bool = ...) -> None: ...
+ GROUP_IDS_FIELD_NUMBER: _ClassVar[int]
+ ID_FIELD_NUMBER: _ClassVar[int]
+ KEY_FIELD_NUMBER: _ClassVar[int]
+ LICENSE_START_TIME_FIELD_NUMBER: _ClassVar[int]
+ PLATFORM_VERIFICATION_STATUS_FIELD_NUMBER: _ClassVar[int]
+ POLICY_FIELD_NUMBER: _ClassVar[int]
+ PROTECTION_SCHEME_FIELD_NUMBER: _ClassVar[int]
+ PROVIDER_CLIENT_TOKEN_FIELD_NUMBER: _ClassVar[int]
+ REMOTE_ATTESTATION_VERIFIED_FIELD_NUMBER: _ClassVar[int]
+ SRM_REQUIREMENT_FIELD_NUMBER: _ClassVar[int]
+ SRM_UPDATE_FIELD_NUMBER: _ClassVar[int]
+ group_ids: _containers.RepeatedScalarFieldContainer[bytes]
+ id: LicenseIdentification
+ key: _containers.RepeatedCompositeFieldContainer[License.KeyContainer]
+ license_start_time: int
+ platform_verification_status: PlatformVerificationStatus
+ policy: License.Policy
+ protection_scheme: int
+ provider_client_token: bytes
+ remote_attestation_verified: bool
+ srm_requirement: bytes
+ srm_update: bytes
+ def __init__(self, id: _Optional[_Union[LicenseIdentification, _Mapping]] = ..., policy: _Optional[_Union[License.Policy, _Mapping]] = ..., key: _Optional[_Iterable[_Union[License.KeyContainer, _Mapping]]] = ..., license_start_time: _Optional[int] = ..., remote_attestation_verified: bool = ..., provider_client_token: _Optional[bytes] = ..., protection_scheme: _Optional[int] = ..., srm_requirement: _Optional[bytes] = ..., srm_update: _Optional[bytes] = ..., platform_verification_status: _Optional[_Union[PlatformVerificationStatus, str]] = ..., group_ids: _Optional[_Iterable[bytes]] = ...) -> None: ...
+
+class LicenseIdentification(_message.Message):
+ __slots__ = ["provider_session_token", "purchase_id", "request_id", "session_id", "type", "version"]
+ PROVIDER_SESSION_TOKEN_FIELD_NUMBER: _ClassVar[int]
+ PURCHASE_ID_FIELD_NUMBER: _ClassVar[int]
+ REQUEST_ID_FIELD_NUMBER: _ClassVar[int]
+ SESSION_ID_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ VERSION_FIELD_NUMBER: _ClassVar[int]
+ provider_session_token: bytes
+ purchase_id: bytes
+ request_id: bytes
+ session_id: bytes
+ type: LicenseType
+ version: int
+ def __init__(self, request_id: _Optional[bytes] = ..., session_id: _Optional[bytes] = ..., purchase_id: _Optional[bytes] = ..., type: _Optional[_Union[LicenseType, str]] = ..., version: _Optional[int] = ..., provider_session_token: _Optional[bytes] = ...) -> None: ...
+
+class LicenseRequest(_message.Message):
+ __slots__ = ["client_id", "content_id", "encrypted_client_id", "key_control_nonce", "key_control_nonce_deprecated", "protocol_version", "request_time", "type"]
+ class RequestType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class ContentIdentification(_message.Message):
+ __slots__ = ["existing_license", "init_data", "webm_key_id", "widevine_pssh_data"]
+ class ExistingLicense(_message.Message):
+ __slots__ = ["license_id", "seconds_since_last_played", "seconds_since_started", "session_usage_table_entry"]
+ LICENSE_ID_FIELD_NUMBER: _ClassVar[int]
+ SECONDS_SINCE_LAST_PLAYED_FIELD_NUMBER: _ClassVar[int]
+ SECONDS_SINCE_STARTED_FIELD_NUMBER: _ClassVar[int]
+ SESSION_USAGE_TABLE_ENTRY_FIELD_NUMBER: _ClassVar[int]
+ license_id: LicenseIdentification
+ seconds_since_last_played: int
+ seconds_since_started: int
+ session_usage_table_entry: bytes
+ def __init__(self, license_id: _Optional[_Union[LicenseIdentification, _Mapping]] = ..., seconds_since_started: _Optional[int] = ..., seconds_since_last_played: _Optional[int] = ..., session_usage_table_entry: _Optional[bytes] = ...) -> None: ...
+ class InitData(_message.Message):
+ __slots__ = ["init_data", "init_data_type", "license_type", "request_id"]
+ class InitDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ CENC: LicenseRequest.ContentIdentification.InitData.InitDataType
+ INIT_DATA_FIELD_NUMBER: _ClassVar[int]
+ INIT_DATA_TYPE_FIELD_NUMBER: _ClassVar[int]
+ LICENSE_TYPE_FIELD_NUMBER: _ClassVar[int]
+ REQUEST_ID_FIELD_NUMBER: _ClassVar[int]
+ WEBM: LicenseRequest.ContentIdentification.InitData.InitDataType
+ init_data: bytes
+ init_data_type: LicenseRequest.ContentIdentification.InitData.InitDataType
+ license_type: LicenseType
+ request_id: bytes
+ def __init__(self, init_data_type: _Optional[_Union[LicenseRequest.ContentIdentification.InitData.InitDataType, str]] = ..., init_data: _Optional[bytes] = ..., license_type: _Optional[_Union[LicenseType, str]] = ..., request_id: _Optional[bytes] = ...) -> None: ...
+ class WebmKeyId(_message.Message):
+ __slots__ = ["header", "license_type", "request_id"]
+ HEADER_FIELD_NUMBER: _ClassVar[int]
+ LICENSE_TYPE_FIELD_NUMBER: _ClassVar[int]
+ REQUEST_ID_FIELD_NUMBER: _ClassVar[int]
+ header: bytes
+ license_type: LicenseType
+ request_id: bytes
+ def __init__(self, header: _Optional[bytes] = ..., license_type: _Optional[_Union[LicenseType, str]] = ..., request_id: _Optional[bytes] = ...) -> None: ...
+ class WidevinePsshData(_message.Message):
+ __slots__ = ["license_type", "pssh_data", "request_id"]
+ LICENSE_TYPE_FIELD_NUMBER: _ClassVar[int]
+ PSSH_DATA_FIELD_NUMBER: _ClassVar[int]
+ REQUEST_ID_FIELD_NUMBER: _ClassVar[int]
+ license_type: LicenseType
+ pssh_data: _containers.RepeatedScalarFieldContainer[bytes]
+ request_id: bytes
+ def __init__(self, pssh_data: _Optional[_Iterable[bytes]] = ..., license_type: _Optional[_Union[LicenseType, str]] = ..., request_id: _Optional[bytes] = ...) -> None: ...
+ EXISTING_LICENSE_FIELD_NUMBER: _ClassVar[int]
+ INIT_DATA_FIELD_NUMBER: _ClassVar[int]
+ WEBM_KEY_ID_FIELD_NUMBER: _ClassVar[int]
+ WIDEVINE_PSSH_DATA_FIELD_NUMBER: _ClassVar[int]
+ existing_license: LicenseRequest.ContentIdentification.ExistingLicense
+ init_data: LicenseRequest.ContentIdentification.InitData
+ webm_key_id: LicenseRequest.ContentIdentification.WebmKeyId
+ widevine_pssh_data: LicenseRequest.ContentIdentification.WidevinePsshData
+ def __init__(self, widevine_pssh_data: _Optional[_Union[LicenseRequest.ContentIdentification.WidevinePsshData, _Mapping]] = ..., webm_key_id: _Optional[_Union[LicenseRequest.ContentIdentification.WebmKeyId, _Mapping]] = ..., existing_license: _Optional[_Union[LicenseRequest.ContentIdentification.ExistingLicense, _Mapping]] = ..., init_data: _Optional[_Union[LicenseRequest.ContentIdentification.InitData, _Mapping]] = ...) -> None: ...
+ CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
+ CONTENT_ID_FIELD_NUMBER: _ClassVar[int]
+ ENCRYPTED_CLIENT_ID_FIELD_NUMBER: _ClassVar[int]
+ KEY_CONTROL_NONCE_DEPRECATED_FIELD_NUMBER: _ClassVar[int]
+ KEY_CONTROL_NONCE_FIELD_NUMBER: _ClassVar[int]
+ NEW: LicenseRequest.RequestType
+ PROTOCOL_VERSION_FIELD_NUMBER: _ClassVar[int]
+ RELEASE: LicenseRequest.RequestType
+ RENEWAL: LicenseRequest.RequestType
+ REQUEST_TIME_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ client_id: ClientIdentification
+ content_id: LicenseRequest.ContentIdentification
+ encrypted_client_id: EncryptedClientIdentification
+ key_control_nonce: int
+ key_control_nonce_deprecated: bytes
+ protocol_version: ProtocolVersion
+ request_time: int
+ type: LicenseRequest.RequestType
+ def __init__(self, client_id: _Optional[_Union[ClientIdentification, _Mapping]] = ..., content_id: _Optional[_Union[LicenseRequest.ContentIdentification, _Mapping]] = ..., type: _Optional[_Union[LicenseRequest.RequestType, str]] = ..., request_time: _Optional[int] = ..., key_control_nonce_deprecated: _Optional[bytes] = ..., protocol_version: _Optional[_Union[ProtocolVersion, str]] = ..., key_control_nonce: _Optional[int] = ..., encrypted_client_id: _Optional[_Union[EncryptedClientIdentification, _Mapping]] = ...) -> None: ...
+
+class MetricData(_message.Message):
+ __slots__ = ["metric_data", "stage_name"]
+ class MetricType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class TypeValue(_message.Message):
+ __slots__ = ["type", "value"]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ VALUE_FIELD_NUMBER: _ClassVar[int]
+ type: MetricData.MetricType
+ value: int
+ def __init__(self, type: _Optional[_Union[MetricData.MetricType, str]] = ..., value: _Optional[int] = ...) -> None: ...
+ LATENCY: MetricData.MetricType
+ METRIC_DATA_FIELD_NUMBER: _ClassVar[int]
+ STAGE_NAME_FIELD_NUMBER: _ClassVar[int]
+ TIMESTAMP: MetricData.MetricType
+ metric_data: _containers.RepeatedCompositeFieldContainer[MetricData.TypeValue]
+ stage_name: str
+ def __init__(self, stage_name: _Optional[str] = ..., metric_data: _Optional[_Iterable[_Union[MetricData.TypeValue, _Mapping]]] = ...) -> None: ...
+
+class SignedDrmCertificate(_message.Message):
+ __slots__ = ["drm_certificate", "hash_algorithm", "signature", "signer"]
+ DRM_CERTIFICATE_FIELD_NUMBER: _ClassVar[int]
+ HASH_ALGORITHM_FIELD_NUMBER: _ClassVar[int]
+ SIGNATURE_FIELD_NUMBER: _ClassVar[int]
+ SIGNER_FIELD_NUMBER: _ClassVar[int]
+ drm_certificate: bytes
+ hash_algorithm: HashAlgorithmProto
+ signature: bytes
+ signer: SignedDrmCertificate
+ def __init__(self, drm_certificate: _Optional[bytes] = ..., signature: _Optional[bytes] = ..., signer: _Optional[_Union[SignedDrmCertificate, _Mapping]] = ..., hash_algorithm: _Optional[_Union[HashAlgorithmProto, str]] = ...) -> None: ...
+
+class SignedMessage(_message.Message):
+ __slots__ = ["metric_data", "msg", "oemcrypto_core_message", "remote_attestation", "service_version_info", "session_key", "session_key_type", "signature", "type"]
+ class MessageType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class SessionKeyType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ CAS_LICENSE: SignedMessage.MessageType
+ CAS_LICENSE_REQUEST: SignedMessage.MessageType
+ EPHERMERAL_ECC_PUBLIC_KEY: SignedMessage.SessionKeyType
+ ERROR_RESPONSE: SignedMessage.MessageType
+ EXTERNAL_LICENSE: SignedMessage.MessageType
+ EXTERNAL_LICENSE_REQUEST: SignedMessage.MessageType
+ LICENSE: SignedMessage.MessageType
+ LICENSE_REQUEST: SignedMessage.MessageType
+ METRIC_DATA_FIELD_NUMBER: _ClassVar[int]
+ MSG_FIELD_NUMBER: _ClassVar[int]
+ OEMCRYPTO_CORE_MESSAGE_FIELD_NUMBER: _ClassVar[int]
+ REMOTE_ATTESTATION_FIELD_NUMBER: _ClassVar[int]
+ SERVICE_CERTIFICATE: SignedMessage.MessageType
+ SERVICE_CERTIFICATE_REQUEST: SignedMessage.MessageType
+ SERVICE_VERSION_INFO_FIELD_NUMBER: _ClassVar[int]
+ SESSION_KEY_FIELD_NUMBER: _ClassVar[int]
+ SESSION_KEY_TYPE_FIELD_NUMBER: _ClassVar[int]
+ SIGNATURE_FIELD_NUMBER: _ClassVar[int]
+ SUB_LICENSE: SignedMessage.MessageType
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ UNDEFINED: SignedMessage.SessionKeyType
+ WRAPPED_AES_KEY: SignedMessage.SessionKeyType
+ metric_data: _containers.RepeatedCompositeFieldContainer[MetricData]
+ msg: bytes
+ oemcrypto_core_message: bytes
+ remote_attestation: bytes
+ service_version_info: VersionInfo
+ session_key: bytes
+ session_key_type: SignedMessage.SessionKeyType
+ signature: bytes
+ type: SignedMessage.MessageType
+ def __init__(self, type: _Optional[_Union[SignedMessage.MessageType, str]] = ..., msg: _Optional[bytes] = ..., signature: _Optional[bytes] = ..., session_key: _Optional[bytes] = ..., remote_attestation: _Optional[bytes] = ..., metric_data: _Optional[_Iterable[_Union[MetricData, _Mapping]]] = ..., service_version_info: _Optional[_Union[VersionInfo, _Mapping]] = ..., session_key_type: _Optional[_Union[SignedMessage.SessionKeyType, str]] = ..., oemcrypto_core_message: _Optional[bytes] = ...) -> None: ...
+
+class VersionInfo(_message.Message):
+ __slots__ = ["license_sdk_version", "license_service_version"]
+ LICENSE_SDK_VERSION_FIELD_NUMBER: _ClassVar[int]
+ LICENSE_SERVICE_VERSION_FIELD_NUMBER: _ClassVar[int]
+ license_sdk_version: str
+ license_service_version: str
+ def __init__(self, license_sdk_version: _Optional[str] = ..., license_service_version: _Optional[str] = ...) -> None: ...
+
+class WidevinePsshData(_message.Message):
+ __slots__ = ["algorithm", "content_id", "crypto_period_index", "crypto_period_seconds", "entitled_keys", "group_ids", "grouped_license", "key_ids", "key_sequence", "policy", "protection_scheme", "provider", "track_type", "type", "video_feature"]
+ class Algorithm(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class Type(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+ class EntitledKey(_message.Message):
+ __slots__ = ["entitlement_key_id", "entitlement_key_size_bytes", "iv", "key", "key_id"]
+ ENTITLEMENT_KEY_ID_FIELD_NUMBER: _ClassVar[int]
+ ENTITLEMENT_KEY_SIZE_BYTES_FIELD_NUMBER: _ClassVar[int]
+ IV_FIELD_NUMBER: _ClassVar[int]
+ KEY_FIELD_NUMBER: _ClassVar[int]
+ KEY_ID_FIELD_NUMBER: _ClassVar[int]
+ entitlement_key_id: bytes
+ entitlement_key_size_bytes: int
+ iv: bytes
+ key: bytes
+ key_id: bytes
+ def __init__(self, entitlement_key_id: _Optional[bytes] = ..., key_id: _Optional[bytes] = ..., key: _Optional[bytes] = ..., iv: _Optional[bytes] = ..., entitlement_key_size_bytes: _Optional[int] = ...) -> None: ...
+ AESCTR: WidevinePsshData.Algorithm
+ ALGORITHM_FIELD_NUMBER: _ClassVar[int]
+ CONTENT_ID_FIELD_NUMBER: _ClassVar[int]
+ CRYPTO_PERIOD_INDEX_FIELD_NUMBER: _ClassVar[int]
+ CRYPTO_PERIOD_SECONDS_FIELD_NUMBER: _ClassVar[int]
+ ENTITLED_KEY: WidevinePsshData.Type
+ ENTITLED_KEYS_FIELD_NUMBER: _ClassVar[int]
+ ENTITLEMENT: WidevinePsshData.Type
+ GROUPED_LICENSE_FIELD_NUMBER: _ClassVar[int]
+ GROUP_IDS_FIELD_NUMBER: _ClassVar[int]
+ KEY_IDS_FIELD_NUMBER: _ClassVar[int]
+ KEY_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
+ POLICY_FIELD_NUMBER: _ClassVar[int]
+ PROTECTION_SCHEME_FIELD_NUMBER: _ClassVar[int]
+ PROVIDER_FIELD_NUMBER: _ClassVar[int]
+ SINGLE: WidevinePsshData.Type
+ TRACK_TYPE_FIELD_NUMBER: _ClassVar[int]
+ TYPE_FIELD_NUMBER: _ClassVar[int]
+ UNENCRYPTED: WidevinePsshData.Algorithm
+ VIDEO_FEATURE_FIELD_NUMBER: _ClassVar[int]
+ algorithm: WidevinePsshData.Algorithm
+ content_id: bytes
+ crypto_period_index: int
+ crypto_period_seconds: int
+ entitled_keys: _containers.RepeatedCompositeFieldContainer[WidevinePsshData.EntitledKey]
+ group_ids: _containers.RepeatedScalarFieldContainer[bytes]
+ grouped_license: bytes
+ key_ids: _containers.RepeatedScalarFieldContainer[bytes]
+ key_sequence: int
+ policy: str
+ protection_scheme: int
+ provider: str
+ track_type: str
+ type: WidevinePsshData.Type
+ video_feature: str
+ def __init__(self, key_ids: _Optional[_Iterable[bytes]] = ..., content_id: _Optional[bytes] = ..., crypto_period_index: _Optional[int] = ..., protection_scheme: _Optional[int] = ..., crypto_period_seconds: _Optional[int] = ..., type: _Optional[_Union[WidevinePsshData.Type, str]] = ..., key_sequence: _Optional[int] = ..., group_ids: _Optional[_Iterable[bytes]] = ..., entitled_keys: _Optional[_Iterable[_Union[WidevinePsshData.EntitledKey, _Mapping]]] = ..., video_feature: _Optional[str] = ..., algorithm: _Optional[_Union[WidevinePsshData.Algorithm, str]] = ..., provider: _Optional[str] = ..., track_type: _Optional[str] = ..., policy: _Optional[str] = ..., grouped_license: _Optional[bytes] = ...) -> None: ...
+
+class LicenseType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+
+class PlatformVerificationStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+
+class ProtocolVersion(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
+
+class HashAlgorithmProto(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
+ __slots__ = []
diff --git a/pywidevine/main.py b/pywidevine/main.py
new file mode 100644
index 0000000..bef2dd5
--- /dev/null
+++ b/pywidevine/main.py
@@ -0,0 +1,398 @@
+import logging
+from datetime import datetime
+from pathlib import Path
+from typing import Optional
+from zlib import crc32
+
+import click
+import requests
+import yaml
+from construct import ConstructError
+from google.protobuf.json_format import MessageToDict
+from unidecode import UnidecodeError, unidecode
+
+from pywidevine import __version__
+from pywidevine.cdm import Cdm
+from pywidevine.device import Device, DeviceTypes
+from pywidevine.license_protocol_pb2 import FileHashes, LicenseType
+from pywidevine.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:
+ """pywidevine—Python Widevine CDM implementation."""
+ logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)
+ log = logging.getLogger()
+
+ current_year = datetime.now().year
+ copyright_years = f"2022-{current_year}"
+
+ log.info("pywidevine version %s Copyright (c) %s rlaphoenix, DevLARLEY", __version__, copyright_years)
+ log.info("https://github.com/devine-dl/pywidevine")
+ if version:
+ return
+
+
+@main.command(name="license")
+@click.argument("device_path", type=Path)
+@click.argument("pssh", type=PSSH)
+@click.argument("server", type=str)
+@click.option("-t", "--type", "license_type", type=click.Choice(LicenseType.keys(), case_sensitive=False),
+ default="STREAMING",
+ help="License Type to Request.")
+@click.option("-p", "--privacy", is_flag=True, default=False,
+ help="Use Privacy Mode, off by default.")
+def license_(device_path: Path, pssh: PSSH, server: str, license_type: str, privacy: bool) -> None:
+ """
+ Make a License Request for PSSH to SERVER using DEVICE.
+ It will return a list of all keys within the returned license.
+
+ This expects the Licence Server to be a simple opaque interface where the Challenge
+ is sent as is (as bytes), and the License response is returned as is (as bytes).
+ This is a common behavior for some License Servers and is our only option for a generic
+ licensing function.
+
+ You may modify this function to change how it sends the Challenge and how it parses
+ the License response. However, for non-generic license calls, I recommend creating a
+ new script that imports and uses the pywidevine module instead. This generic function
+ is only useful as a quick generic license call.
+
+ This is also a great way of showing you how to use pywidevine in your own projects.
+ """
+ log = logging.getLogger("license")
+
+ # load device
+ device = Device.load(device_path)
+ log.info("[+] Loaded Device (%s L%s)", device.system_id, device.security_level)
+ log.debug(device)
+
+ # load cdm
+ cdm = Cdm.from_device(device)
+ log.info("[+] Loaded CDM")
+ log.debug(cdm)
+
+ # open cdm session
+ session_id = cdm.open()
+ log.info("[+] Opened CDM Session: %s", session_id.hex())
+
+ if privacy:
+ # get service cert for license server via cert challenge
+ service_cert_res = requests.post(
+ url=server,
+ data=cdm.service_certificate_challenge
+ )
+ if service_cert_res.status_code != 200:
+ log.error(
+ "[-] Failed to get Service Privacy Certificate: [%s] %s",
+ service_cert_res.status_code,
+ service_cert_res.text
+ )
+ return
+ service_cert = service_cert_res.content
+ provider_id = cdm.set_service_certificate(session_id, service_cert)
+ log.info("[+] Set Service Privacy Certificate: %s", provider_id)
+ log.debug(service_cert)
+
+ # get license challenge
+ challenge = cdm.get_license_challenge(session_id, pssh, license_type, privacy_mode=True)
+ log.info("[+] Created License Request Message (Challenge)")
+ log.debug(challenge)
+
+ # send license challenge
+ license_res = requests.post(
+ url=server,
+ data=challenge
+ )
+ if license_res.status_code != 200:
+ log.error("[-] Failed to send challenge: [%s] %s", license_res.status_code, license_res.text)
+ return
+ licence = license_res.content
+ log.info("[+] Got License Message")
+ log.debug(licence)
+
+ # parse license challenge
+ cdm.parse_license(session_id, licence)
+ log.info("[+] License Parsed Successfully")
+
+ # print keys
+ for key in cdm.get_keys(session_id):
+ log.info("[%s] %s:%s", key.type, key.kid.hex, key.key.hex())
+
+ # close session, disposes of session data
+ cdm.close(session_id)
+
+
+@main.command()
+@click.argument("device", type=Path)
+@click.option("-p", "--privacy", is_flag=True, default=False,
+ help="Use Privacy Mode, off by default.")
+@click.pass_context
+def test(ctx: click.Context, device: Path, privacy: bool) -> None:
+ """
+ Test the CDM code by getting Content Keys for Bitmovin's Art of Motion example.
+ https://bitmovin.com/demos/drm
+ https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd
+
+ The device argument is a Path to a Widevine Device (.wvd) file which contains
+ the device private key among other required information.
+ """
+ # The PSSH is the same for all tracks both video and audio.
+ # However, this might not be the case for all services/manifests.
+ pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa"
+ "7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==")
+
+ # This License Server requires no authorization at all, no cookies, no credentials
+ # nothing. This is often not the case for real services.
+ license_server = "https://cwip-shaka-proxy.appspot.com/no_auth"
+
+ # Specify OFFLINE if it's a PSSH for a download/offline mode title, e.g., the
+ # Download feature on Netflix Apps. Otherwise, use STREAMING or AUTOMATIC.
+ license_type = "STREAMING"
+
+ # this runs the `cdm license` CLI-command code with the data we set above
+ # it will print information as it goes to the terminal
+ ctx.invoke(
+ license_,
+ device_path=device,
+ pssh=pssh,
+ server=license_server,
+ license_type=license_type,
+ privacy=privacy
+ )
+
+
+@main.command()
+@click.option("-t", "--type", "type_", type=click.Choice([x.name for x in DeviceTypes], case_sensitive=False),
+ required=True, help="Device Type")
+@click.option("-l", "--level", type=click.IntRange(1, 3), required=True, help="Device Security Level")
+@click.option("-k", "--key", type=Path, required=True, help="Device RSA Private Key in PEM or DER format")
+@click.option("-c", "--client_id", type=Path, required=True, help="Widevine ClientIdentification Blob file")
+@click.option("-v", "--vmp", type=Path, default=None, help="Widevine FileHashes Blob file")
+@click.option("-o", "--output", type=Path, default=None, help="Output Path or Directory")
+@click.pass_context
+def create_device(
+ ctx: click.Context,
+ type_: str,
+ level: int,
+ key: Path,
+ client_id: Path,
+ vmp: Optional[Path] = None,
+ output: Optional[Path] = None
+) -> None:
+ """
+ Create a Widevine Device (.wvd) file from an RSA Private Key (PEM or DER) and Client ID Blob.
+ Optionally also a VMP (Verified Media Path) Blob, which will be stored in the Client ID.
+ """
+ if not key.is_file():
+ raise click.UsageError("key: Not a path to a file, or it doesn't exist.", ctx)
+ if not client_id.is_file():
+ raise click.UsageError("client_id: Not a path to a file, or it doesn't exist.", ctx)
+ if vmp and not vmp.is_file():
+ raise click.UsageError("vmp: Not a path to a file, or it doesn't exist.", ctx)
+
+ log = logging.getLogger("create-device")
+
+ device = Device(
+ type_=DeviceTypes[type_.upper()],
+ security_level=level,
+ flags=None,
+ private_key=key.read_bytes(),
+ client_id=client_id.read_bytes()
+ )
+
+ if vmp:
+ new_vmp_data = vmp.read_bytes()
+ if device.client_id.vmp_data and device.client_id.vmp_data != new_vmp_data:
+ log.warning("Client ID already has Verified Media Path data")
+ device.client_id.vmp_data = new_vmp_data
+
+ client_info = {}
+ for entry in device.client_id.client_info:
+ client_info[entry.name] = entry.value
+
+ wvd_bin = device.dumps()
+
+ name = f"{client_info['company_name']} {client_info['model_name']}"
+ if client_info.get("widevine_cdm_version"):
+ name += f" {client_info['widevine_cdm_version']}"
+ name += f" {crc32(wvd_bin).to_bytes(4, 'big').hex()}"
+
+ try:
+ name = unidecode(name.strip().lower().replace(" ", "_"))
+ except UnidecodeError as e:
+ raise click.ClickException(f"Failed to sanitize name, {e}")
+
+ if output and output.suffix:
+ if output.suffix.lower() != ".wvd":
+ log.warning(f"Saving WVD with the file extension '{output.suffix}' but '.wvd' is recommended.")
+ out_path = output
+ else:
+ out_dir = output or Path.cwd()
+ out_path = out_dir / f"{name}_{device.system_id}_l{device.security_level}.wvd"
+
+ 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(wvd_bin)
+
+ log.info("Created Widevine Device (.wvd) file, %s", out_path.name)
+ log.info(" + Type: %s", device.type.name)
+ log.info(" + System ID: %s", device.system_id)
+ log.info(" + Security Level: %s", device.security_level)
+ log.info(" + Flags: %s", device.flags)
+ log.info(" + Private Key: %s (%s bit)", bool(device.private_key), device.private_key.size_in_bits())
+ log.info(" + Client ID: %s (%s bytes)", bool(device.client_id), len(device.client_id.SerializeToString()))
+ if device.client_id.vmp_data:
+ file_hashes_ = FileHashes()
+ file_hashes_.ParseFromString(device.client_id.vmp_data)
+ log.info(" + VMP: True (%s signatures)", len(file_hashes_.signatures))
+ else:
+ log.info(" + VMP: False")
+ log.info(" + Saved to: %s", out_path.absolute())
+
+
+@main.command()
+@click.argument("wvd_path", type=Path)
+@click.option("-o", "--out_dir", type=Path, default=None, help="Output Directory")
+@click.pass_context
+def export_device(ctx: click.Context, wvd_path: Path, out_dir: Optional[Path] = None) -> None:
+ """
+ Export a Widevine Device (.wvd) file to an RSA Private Key (PEM and DER) and Client ID Blob.
+ Optionally also a VMP (Verified Media Path) Blob, which will be stored in the Client ID.
+
+ If an output directory is not specified, it will be stored in the current working directory.
+ """
+ if not wvd_path.is_file():
+ raise click.UsageError("wvd_path: Not a path to a file, or it doesn't exist.", ctx)
+
+ log = logging.getLogger("export-device")
+ log.info("Exporting Widevine Device (.wvd) file, %s", wvd_path.stem)
+
+ if not out_dir:
+ out_dir = Path.cwd()
+
+ out_path = out_dir / wvd_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(wvd_path)
+
+ log.info(f"L{device.security_level} {device.system_id} {device.type.name}")
+ log.info(f"Saving to: {out_path}")
+
+ device_meta = {
+ "wvd": {
+ "device_type": device.type.name,
+ "security_level": device.security_level,
+ **device.flags
+ },
+ "client_info": {},
+ "capabilities": MessageToDict(device.client_id, preserving_proto_field_name=True)["client_capabilities"]
+ }
+ for client_info in device.client_id.client_info:
+ device_meta["client_info"][client_info.name] = client_info.value
+
+ device_meta_path = out_path / "metadata.yml"
+ device_meta_path.write_text(yaml.dump(device_meta), encoding="utf8")
+ log.info("Exported Device Metadata as metadata.yml")
+
+ if device.private_key:
+ private_key_path = out_path / "private_key.pem"
+ private_key_path.write_text(
+ data=device.private_key.export_key().decode(),
+ encoding="utf8"
+ )
+ private_key_path.with_suffix(".der").write_bytes(
+ device.private_key.export_key(format="DER")
+ )
+ log.info("Exported Private Key as private_key.der and private_key.pem")
+ else:
+ log.warning("No Private Key available")
+
+ if device.client_id:
+ client_id_path = out_path / "client_id.bin"
+ client_id_path.write_bytes(device.client_id.SerializeToString())
+ log.info("Exported Client ID as client_id.bin")
+ else:
+ log.warning("No Client ID available")
+
+ if device.client_id.vmp_data:
+ vmp_path = out_path / "vmp.bin"
+ vmp_path.write_bytes(device.client_id.vmp_data)
+ log.info("Exported VMP (File Hashes) as vmp.bin")
+ else:
+ log.info("No VMP (File Hashes) available")
+
+
+@main.command()
+@click.argument("path", type=Path)
+@click.pass_context
+def migrate(ctx: click.Context, path: Path) -> None:
+ """
+ Upgrade from earlier versions of the Widevine Device (.wvd) format.
+
+ The path argument can be a direct path to a Widevine Device (.wvd) file, or a path
+ to a folder of Widevine Devices files.
+
+ The migrated devices are saved to its original location, overwriting the old version.
+ """
+ if not path.exists():
+ raise click.UsageError(f"path: The path '{path}' does not exist.", ctx)
+
+ log = logging.getLogger("migrate")
+
+ if path.is_dir():
+ devices = list(path.glob("*.wvd"))
+ else:
+ devices = [path]
+
+ migrated = 0
+ for device in devices:
+ log.info("Migrating %s...", device.name)
+
+ try:
+ new_device = Device.migrate(device.read_bytes())
+ except (ConstructError, ValueError) as e:
+ log.error(" - %s", e)
+ continue
+
+ log.debug(new_device)
+ new_device.dump(device)
+
+ log.info(" + Success")
+ migrated += 1
+
+ log.info("Migrated %s/%s devices!", migrated, len(devices))
+
+
+@main.command("serve", short_help="Serve your local CDM and Widevine 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=8786, help="Port to serve from.")
+def serve_(config_path: Path, host: str, port: int) -> None:
+ """
+ Serve your local CDM and Widevine Devices Remotely.
+
+ \b
+ [CONFIG] is a path to a serve config file.
+ See `serve.example.yml` for an example config file.
+
+ \b
+ 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 pywidevine import serve # isort:skip
+ import yaml # isort:skip
+
+ config = yaml.safe_load(config_path.read_text(encoding="utf8"))
+ serve.run(config, host, port)
diff --git a/pywidevine/pssh.py b/pywidevine/pssh.py
new file mode 100644
index 0000000..b81111a
--- /dev/null
+++ b/pywidevine/pssh.py
@@ -0,0 +1,442 @@
+from __future__ import annotations
+
+import base64
+import binascii
+import string
+from io import BytesIO
+from typing import Optional, Union
+from uuid import UUID
+from xml.etree.ElementTree import XML
+
+import construct
+from construct import Container
+from google.protobuf.message import DecodeError
+from pymp4.parser import Box
+
+from pywidevine.license_protocol_pb2 import WidevinePsshData
+
+
+class PSSH:
+ """
+ MP4 PSSH Box-related utilities.
+ Allows you to load, create, and modify various kinds of DRM system headers.
+ """
+
+ class SystemId:
+ Widevine = UUID(hex="edef8ba979d64acea3c827dcd51d21ed")
+ PlayReady = UUID(hex="9a04f07998404286ab92e65be0885f95")
+
+ def __init__(self, data: Union[Container, str, bytes], strict: bool = False):
+ """
+ Load a PSSH box, WidevineCencHeader, or PlayReadyHeader.
+
+ When loading a WidevineCencHeader or PlayReadyHeader, a new v0 PSSH box will be
+ created and the header will be parsed and stored in the init_data field. However,
+ PlayReadyHeaders (and PlayReadyObjects) are not yet currently parsed and are
+ stored as bytes.
+
+ [Strict mode (strict=True)]
+
+ Supports the following forms of input data in either Base64 or Bytes form:
+ - Full PSSH mp4 boxes (as defined by pymp4 Box).
+ - Full Widevine Cenc Headers (as defined by WidevinePsshData proto).
+ - Full PlayReady Objects and Headers (as defined by Microsoft Docs).
+
+ [Lenient mode (strict=False, default)]
+
+ If the data is not supported in Strict mode, and is assumed not to be corrupt or
+ parsed incorrectly, the License Server likely accepts a custom init_data value
+ during a License Request call. This is uncommon behavior but not out of realm of
+ possibilities. For example, Netflix does this with it's MSL WidevineExchange
+ scheme.
+
+ Lenient mode will craft a new v0 PSSH box with the init_data field set to
+ the provided data as-is. The data will first be base64 decoded. This behavior
+ may not work in your scenario and if that's the case please manually craft
+ your own PSSH box with the init_data field to be used in License Requests.
+
+ Raises:
+ ValueError: If the data is empty.
+ TypeError: If the data is an unexpected type.
+ binascii.Error: If the data could not be decoded as Base64 if provided as a
+ string.
+ DecodeError: If the data could not be parsed as a PSSH mp4 box nor a Widevine
+ Cenc Header and strict mode is enabled.
+ """
+ if not data:
+ raise ValueError("Data must not be empty.")
+
+ if isinstance(data, Container):
+ box = data
+ else:
+ if isinstance(data, str):
+ try:
+ data = base64.b64decode(data)
+ except (binascii.Error, binascii.Incomplete) as e:
+ raise binascii.Error(f"Could not decode data as Base64, {e}")
+
+ if not isinstance(data, bytes):
+ raise TypeError(f"Expected data to be a {Container}, bytes, or base64, not {data!r}")
+
+ try:
+ box = Box.parse(data)
+ except (IOError, construct.ConstructError): # not a box
+ try:
+ widevine_pssh_data = WidevinePsshData()
+ widevine_pssh_data.ParseFromString(data)
+ data_serialized = widevine_pssh_data.SerializeToString()
+ if data_serialized != data: # not actually a WidevinePsshData
+ raise DecodeError()
+ box = Box.parse(Box.build(dict(
+ type=b"pssh",
+ version=0,
+ flags=0,
+ system_ID=PSSH.SystemId.Widevine,
+ init_data=data_serialized
+ )))
+ except DecodeError: # not a widevine cenc header
+ if "".encode("utf-16-le") in data:
+ # TODO: Actually parse `data` as a PlayReadyHeader object and store that instead
+ box = Box.parse(Box.build(dict(
+ type=b"pssh",
+ version=0,
+ flags=0,
+ system_ID=PSSH.SystemId.PlayReady,
+ init_data=data
+ )))
+ elif strict:
+ raise DecodeError(f"Could not parse data as a {Container} nor a {WidevinePsshData}.")
+ else:
+ # Data is not a WidevineCencHeader nor a PlayReadyHeader.
+ # The license server likely has something custom to parse it.
+ # See doc-string about Lenient mode for more information.
+ box = Box.parse(Box.build(dict(
+ type=b"pssh",
+ version=0,
+ flags=0,
+ system_ID=PSSH.SystemId.Widevine,
+ init_data=data
+ )))
+
+ self.version = box.version
+ self.flags = box.flags
+ self.system_id = box.system_ID
+ self.__key_ids = box.key_IDs
+ self.init_data = box.init_data
+
+ def __repr__(self) -> str:
+ return f"PSSH<{self.system_id}>(v{self.version}; {self.flags}, {self.key_ids}, {self.init_data})"
+
+ def __str__(self) -> str:
+ return self.dumps()
+
+ @classmethod
+ def new(
+ cls,
+ system_id: UUID,
+ key_ids: Optional[list[Union[UUID, str, bytes]]] = None,
+ init_data: Optional[Union[WidevinePsshData, str, bytes]] = None,
+ version: int = 0,
+ flags: int = 0
+ ) -> PSSH:
+ """Craft a new version 0 or 1 PSSH Box."""
+ if not system_id:
+ raise ValueError("A System ID must be specified.")
+ if not isinstance(system_id, UUID):
+ raise TypeError(f"Expected system_id to be a UUID, not {system_id!r}")
+
+ if key_ids is not None and not isinstance(key_ids, list):
+ raise TypeError(f"Expected key_ids to be a list not {key_ids!r}")
+
+ if init_data is not None and not isinstance(init_data, (WidevinePsshData, str, bytes)):
+ raise TypeError(f"Expected init_data to be a {WidevinePsshData}, base64, or bytes, not {init_data!r}")
+
+ if not isinstance(version, int):
+ raise TypeError(f"Expected version to be an int not {version!r}")
+ if version not in (0, 1):
+ raise ValueError(f"Invalid version, must be either 0 or 1, not {version}.")
+
+ if not isinstance(flags, int):
+ raise TypeError(f"Expected flags to be an int not {flags!r}")
+ if flags < 0:
+ raise ValueError("Invalid flags, cannot be less than 0.")
+
+ if version == 0 and key_ids is not None and init_data is not None:
+ # v0 boxes use only init_data in the pssh field, but we can use the key_ids within the init_data
+ raise ValueError("Version 0 PSSH boxes must use only init_data, not init_data and key_ids.")
+ elif version == 1:
+ # TODO: I cannot tell if they need either init_data or key_ids exclusively, or both is fine
+ # So for now I will just make sure at least one is supplied
+ if init_data is None and key_ids is None:
+ raise ValueError("Version 1 PSSH boxes must use either init_data or key_ids but neither were provided")
+
+ if init_data is not None:
+ if isinstance(init_data, WidevinePsshData):
+ init_data = init_data.SerializeToString()
+ elif isinstance(init_data, str):
+ if all(c in string.hexdigits for c in init_data):
+ init_data = bytes.fromhex(init_data)
+ else:
+ init_data = base64.b64decode(init_data)
+ elif not isinstance(init_data, bytes):
+ raise TypeError(
+ f"Expecting init_data to be {WidevinePsshData}, hex, base64, or bytes, not {init_data!r}"
+ )
+
+ pssh = cls(Box.parse(Box.build(dict(
+ type=b"pssh",
+ version=version,
+ flags=flags,
+ system_ID=system_id,
+ init_data=[init_data, b""][init_data is None]
+ # key_IDs should not be set yet
+ ))))
+
+ if key_ids:
+ # We must reinforce the version because pymp4 forces v0 if key_IDs is not set.
+ # The set_key_ids() func will set it efficiently in both init_data and the box where needed.
+ # The version must be reinforced ONLY if we have key_id data or there's a possibility of making
+ # a v1 PSSH box, that did not have key_IDs set in the PSSH box.
+ pssh.version = version
+ pssh.set_key_ids(key_ids)
+
+ return pssh
+
+ @property
+ def key_ids(self) -> list[UUID]:
+ """
+ Get all Key IDs from within the Box or Init Data, wherever possible.
+
+ Supports:
+ - Version 1 PSSH Boxes
+ - WidevineCencHeaders
+ - PlayReadyHeaders (4.0.0.0->4.3.0.0)
+ """
+ if self.version == 1 and self.__key_ids:
+ return self.__key_ids
+
+ if self.system_id == PSSH.SystemId.Widevine:
+ # TODO: What if its not a Widevine Cenc Header but the System ID is set as Widevine?
+ cenc_header = WidevinePsshData()
+ cenc_header.ParseFromString(self.init_data)
+ return [
+ # the key_ids value may or may not be hex underlying
+ (
+ UUID(bytes=key_id) if len(key_id) == 16 else # normal
+ UUID(hex=key_id.decode()) if len(key_id) == 32 else # stored as hex
+ UUID(int=int.from_bytes(key_id, "big")) # assuming as number
+ )
+ for key_id in cenc_header.key_ids
+ ]
+
+ if self.system_id == PSSH.SystemId.PlayReady:
+ # Assuming init data is a PRO (PlayReadyObject)
+ # https://learn.microsoft.com/en-us/playready/specifications/playready-header-specification
+ pro_data = BytesIO(self.init_data)
+ pro_length = int.from_bytes(pro_data.read(4), "little")
+ if pro_length != len(self.init_data):
+ raise ValueError("The PlayReadyObject seems to be corrupt (too big or small, or missing data).")
+ pro_record_count = int.from_bytes(pro_data.read(2), "little")
+
+ for _ in range(pro_record_count):
+ prr_type = int.from_bytes(pro_data.read(2), "little")
+ prr_length = int.from_bytes(pro_data.read(2), "little")
+ prr_value = pro_data.read(prr_length)
+ if prr_type != 0x01:
+ # No PlayReady Header, skip and hope for something else
+ # TODO: Add support for Embedded License Stores (0x03)
+ continue
+
+ wrm_ns = {"wrm": "http://schemas.microsoft.com/DRM/2007/03/PlayReadyHeader"}
+ prr_header = XML(prr_value.decode("utf-16-le"))
+ prr_header_version = prr_header.get("version")
+ if prr_header_version == "4.0.0.0":
+ key_ids = [
+ x.text
+ for x in prr_header.findall("./wrm:DATA/wrm:KID", wrm_ns)
+ if x.text
+ ]
+ elif prr_header_version == "4.1.0.0":
+ key_ids = [
+ x.attrib["VALUE"]
+ for x in prr_header.findall("./wrm:DATA/wrm:PROTECTINFO/wrm:KID", wrm_ns)
+ ]
+ elif prr_header_version in ("4.2.0.0", "4.3.0.0"):
+ # TODO: Retain the Encryption Scheme information in v4.3.0.0
+ # This is because some Key IDs can be AES-CTR while some are AES-CBC.
+ # Conversion to WidevineCencHeader could use this information.
+ key_ids = [
+ x.attrib["VALUE"]
+ for x in prr_header.findall("./wrm:DATA/wrm:PROTECTINFO/wrm:KIDS/wrm:KID", wrm_ns)
+ ]
+ else:
+ raise ValueError(f"Unsupported PlayReadyHeader version {prr_header_version}")
+
+ return [
+ UUID(bytes=base64.b64decode(key_id))
+ for key_id in key_ids
+ ]
+
+ raise ValueError("Unsupported PlayReadyObject, no PlayReadyHeader within the object.")
+
+ raise ValueError(f"This PSSH is not supported by key_ids() property, {self.dumps()}")
+
+ def dump(self) -> bytes:
+ """Export the PSSH object as a full PSSH box in bytes form."""
+ return Box.build(dict(
+ type=b"pssh",
+ version=self.version,
+ flags=self.flags,
+ system_ID=self.system_id,
+ key_IDs=self.key_ids if self.version == 1 and self.key_ids else None,
+ init_data=self.init_data
+ ))
+
+ def dumps(self) -> str:
+ """Export the PSSH object as a full PSSH box in base64 form."""
+ return base64.b64encode(self.dump()).decode()
+
+ def to_widevine(self) -> None:
+ """
+ Convert PlayReady PSSH data to Widevine PSSH data.
+
+ There's only a limited amount of information within a PlayReady PSSH header that
+ can be used in a Widevine PSSH Header. The converted data may or may not result
+ in an accepted PSSH. It depends on what the License Server is expecting.
+ """
+ if self.system_id == PSSH.SystemId.Widevine:
+ raise ValueError("This is already a Widevine PSSH")
+
+ widevine_pssh_data = WidevinePsshData(
+ key_ids=[x.bytes for x in self.key_ids],
+ algorithm="AESCTR"
+ )
+
+ if self.version == 1:
+ # ensure both cenc header and box has same Key IDs
+ # v1 uses both this and within init data for basically no reason
+ self.__key_ids = self.key_ids
+
+ self.init_data = widevine_pssh_data.SerializeToString()
+ self.system_id = PSSH.SystemId.Widevine
+
+ def to_playready(
+ self,
+ la_url: Optional[str] = None,
+ lui_url: Optional[str] = None,
+ ds_id: Optional[bytes] = None,
+ decryptor_setup: Optional[str] = None,
+ custom_data: Optional[str] = None
+ ) -> None:
+ """
+ Convert Widevine PSSH data to PlayReady v4.3.0.0 PSSH data.
+
+ Note that it is impossible to create the CHECKSUM values for AES-CTR Key IDs
+ as you must encrypt the Key ID with the Content Encryption Key using AES-ECB.
+ This may cause software incompatibilities.
+
+ Parameters:
+ la_url: Contains the URL for the license acquisition Web service.
+ Only absolute URLs are allowed.
+ lui_url: Contains the URL for the license acquisition Web service.
+ Only absolute URLs are allowed.
+ ds_id: Service ID for the domain service.
+ decryptor_setup: This tag may only contain the value "ONDEMAND". It
+ indicates to an application that it should not expect the full
+ license chain for the content to be available for acquisition, or
+ already present on the client machine, prior to setting up the
+ media graph. If this tag is not set then it indicates that an
+ application can enforce the license to be acquired, or already
+ present on the client machine, prior to setting up the media graph.
+ custom_data: The content author can add custom XML inside this
+ element. Microsoft code does not act on any data contained inside
+ this element. The Syntax of this params XML is not validated.
+ """
+ if self.system_id == PSSH.SystemId.PlayReady:
+ raise ValueError("This is already a PlayReady PSSH")
+
+ key_ids_xml = ""
+ for key_id in self.key_ids:
+ # Note that it's impossible to create the CHECKSUM value without the Key for the KID
+ key_ids_xml += f"""
+
+ """
+
+ prr_value = f"""
+
+
+
+ {key_ids_xml}
+
+ {'%s' % la_url if la_url else ''}
+ {'%s' % lui_url if lui_url else ''}
+ {'%s' % base64.b64encode(ds_id).decode() if ds_id else ''}
+ {'%s' % decryptor_setup if decryptor_setup else ''}
+ {'%s' % custom_data if custom_data else ''}
+
+
+ """.encode("utf-16-le")
+
+ prr_length = len(prr_value).to_bytes(2, "little")
+ prr_type = (1).to_bytes(2, "little") # Has PlayReadyHeader
+ pro_record_count = (1).to_bytes(2, "little")
+ pro = pro_record_count + prr_type + prr_length + prr_value
+ pro = (len(pro) + 4).to_bytes(4, "little") + pro
+
+ self.init_data = pro
+ self.system_id = PSSH.SystemId.PlayReady
+
+ def set_key_ids(self, key_ids: list[Union[UUID, str, bytes]]) -> None:
+ """Overwrite all Key IDs with the specified Key IDs."""
+ if self.system_id != PSSH.SystemId.Widevine:
+ # TODO: Add support for setting the Key IDs in a PlayReady Header
+ raise ValueError(f"Only Widevine PSSH Boxes are supported, not {self.system_id}.")
+
+ key_id_uuids = self.parse_key_ids(key_ids)
+
+ if self.version == 1 or self.__key_ids:
+ # only use v1 box key_ids if version is 1, or it's already being used
+ # this is in case the service stupidly expects it for version 0
+ self.__key_ids = key_id_uuids
+
+ cenc_header = WidevinePsshData()
+ cenc_header.ParseFromString(self.init_data)
+
+ cenc_header.key_ids[:] = [
+ key_id.bytes
+ for key_id in key_id_uuids
+ ]
+
+ self.init_data = cenc_header.SerializeToString()
+
+ @staticmethod
+ def parse_key_ids(key_ids: list[Union[UUID, str, bytes]]) -> list[UUID]:
+ """
+ Parse a list of Key IDs in hex, base64, or bytes to UUIDs.
+
+ Raises TypeError if `key_ids` is not a list, or the list contains one
+ or more items that are not a UUID, str, or bytes object.
+ """
+ if not isinstance(key_ids, list):
+ raise TypeError(f"Expected key_ids to be a list, not {key_ids!r}")
+
+ if not all(isinstance(x, (UUID, str, bytes)) for x in key_ids):
+ raise TypeError("Some items of key_ids are not a UUID, str, or bytes. Unsure how to continue...")
+
+ uuids = [
+ UUID(bytes=key_id_b)
+ for key_id in key_ids
+ for key_id_b in [
+ key_id.bytes if isinstance(key_id, UUID) else
+ (
+ bytes.fromhex(key_id) if all(c in string.hexdigits for c in key_id) else
+ base64.b64decode(key_id)
+ ) if isinstance(key_id, str) else
+ key_id
+ ]
+ ]
+
+ return uuids
+
+
+__all__ = ("PSSH",)
diff --git a/pywidevine/py.typed b/pywidevine/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/pywidevine/remotecdm.py b/pywidevine/remotecdm.py
new file mode 100644
index 0000000..7e79ce0
--- /dev/null
+++ b/pywidevine/remotecdm.py
@@ -0,0 +1,300 @@
+from __future__ import annotations
+
+import base64
+import binascii
+import re
+from typing import Optional, Union
+
+import requests
+from Crypto.Hash import SHA1
+from Crypto.PublicKey import RSA
+from Crypto.Signature import pss
+from google.protobuf.message import DecodeError
+
+from pywidevine.cdm import Cdm
+from pywidevine.device import Device, DeviceTypes
+from pywidevine.exceptions import (DeviceMismatch, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
+ SignatureMismatch)
+from pywidevine.key import Key
+from pywidevine.license_protocol_pb2 import (ClientIdentification, License, LicenseType, SignedDrmCertificate,
+ SignedMessage)
+from pywidevine.pssh import PSSH
+
+
+class RemoteCdm(Cdm):
+ """Remote Accessible CDM using pywidevine's serve schema."""
+
+ def __init__(
+ self,
+ device_type: Union[DeviceTypes, str],
+ system_id: int,
+ security_level: int,
+ host: str,
+ secret: str,
+ device_name: str
+ ):
+ """Initialize a Widevine Content Decryption Module (CDM)."""
+ if not device_type:
+ raise ValueError("Device Type must be provided")
+ if isinstance(device_type, str):
+ device_type = DeviceTypes[device_type]
+ if not isinstance(device_type, DeviceTypes):
+ raise TypeError(f"Expected device_type to be a {DeviceTypes!r} not {device_type!r}")
+
+ if not system_id:
+ raise ValueError("System ID must be provided")
+ if not isinstance(system_id, int):
+ raise TypeError(f"Expected system_id to be a {int} not {system_id!r}")
+
+ 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.device_type = device_type
+ self.system_id = system_id
+ self.security_level = security_level
+ self.host = host
+ self.device_name = device_name
+
+ # spoof client_id and rsa_key just so we can construct via super call
+ super().__init__(device_type, system_id, security_level, ClientIdentification(), RSA.generate(2048))
+
+ self.__session = requests.Session()
+ self.__session.headers.update({
+ "X-Secret-Key": secret
+ })
+
+ r = requests.head(self.host)
+ if r.status_code != 200:
+ raise ValueError(f"Could not test Remote API version [{r.status_code}]")
+ server = r.headers.get("Server")
+ if not server or "pywidevine serve" not in server.lower():
+ raise ValueError(f"This Remote CDM API does not seem to be a pywidevine serve API ({server}).")
+ server_version_re = re.search(r"pywidevine serve v([\d.]+)", server, re.IGNORECASE)
+ if not server_version_re:
+ raise ValueError("The pywidevine server API is not stating the version correctly, cannot continue.")
+ server_version = server_version_re.group(1)
+ if server_version < "1.4.3":
+ raise ValueError(f"This pywidevine serve API version ({server_version}) is not supported.")
+
+ @classmethod
+ def from_device(cls, device: Device) -> RemoteCdm:
+ raise NotImplementedError("You cannot load a RemoteCdm from a local Device file.")
+
+ def open(self) -> bytes:
+ r = self.__session.get(
+ url=f"{self.host}/{self.device_name}/open"
+ ).json()
+ if r['status'] != 200:
+ raise ValueError(f"Cannot Open CDM Session, {r['message']} [{r['status']}]")
+ r = r["data"]
+
+ if int(r["device"]["system_id"]) != self.system_id:
+ raise DeviceMismatch("The System ID specified does not match the one specified in the API response.")
+
+ if int(r["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(r["session_id"])
+
+ def close(self, session_id: bytes) -> None:
+ r = self.__session.get(
+ url=f"{self.host}/{self.device_name}/close/{session_id.hex()}"
+ ).json()
+ if r["status"] != 200:
+ raise ValueError(f"Cannot Close CDM Session, {r['message']} [{r['status']}]")
+
+ def set_service_certificate(self, session_id: bytes, certificate: Optional[Union[bytes, str]]) -> str:
+ if certificate is None:
+ certificate_b64 = None
+ elif isinstance(certificate, str):
+ certificate_b64 = certificate # assuming base64
+ elif isinstance(certificate, bytes):
+ certificate_b64 = base64.b64encode(certificate).decode()
+ else:
+ raise DecodeError(f"Expecting Certificate to be base64 or bytes, not {certificate!r}")
+
+ r = self.__session.post(
+ url=f"{self.host}/{self.device_name}/set_service_certificate",
+ json={
+ "session_id": session_id.hex(),
+ "certificate": certificate_b64
+ }
+ ).json()
+ if r["status"] != 200:
+ raise ValueError(f"Cannot Set CDMs Service Certificate, {r['message']} [{r['status']}]")
+ r = r["data"]
+
+ return r["provider_id"]
+
+ def get_service_certificate(self, session_id: bytes) -> Optional[SignedDrmCertificate]:
+ r = self.__session.post(
+ url=f"{self.host}/{self.device_name}/get_service_certificate",
+ json={
+ "session_id": session_id.hex()
+ }
+ ).json()
+ if r["status"] != 200:
+ raise ValueError(f"Cannot Get CDMs Service Certificate, {r['message']} [{r['status']}]")
+ r = r["data"]
+
+ service_certificate = r["service_certificate"]
+ if not service_certificate:
+ return None
+
+ service_certificate = base64.b64decode(service_certificate)
+ signed_drm_certificate = SignedDrmCertificate()
+
+ try:
+ signed_drm_certificate.ParseFromString(service_certificate)
+ if signed_drm_certificate.SerializeToString() != service_certificate:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ # could be a direct unsigned DrmCertificate, but reject those anyway
+ raise DecodeError(f"Could not parse certificate as a SignedDrmCertificate, {e}")
+
+ try:
+ pss. \
+ new(RSA.import_key(self.root_cert.public_key)). \
+ verify(
+ msg_hash=SHA1.new(signed_drm_certificate.drm_certificate),
+ signature=signed_drm_certificate.signature
+ )
+ except (ValueError, TypeError):
+ raise SignatureMismatch("Signature Mismatch on SignedDrmCertificate, rejecting certificate")
+
+ return signed_drm_certificate
+
+ def get_license_challenge(
+ self,
+ session_id: bytes,
+ pssh: PSSH,
+ license_type: str = "STREAMING",
+ privacy_mode: bool = True
+ ) -> bytes:
+ if not pssh:
+ raise InvalidInitData("A pssh must be provided.")
+ if not isinstance(pssh, PSSH):
+ raise InvalidInitData(f"Expected pssh to be a {PSSH}, not {pssh!r}")
+
+ if not isinstance(license_type, str):
+ raise InvalidLicenseType(f"Expected license_type to be a {str}, not {license_type!r}")
+ if license_type not in LicenseType.keys():
+ raise InvalidLicenseType(
+ f"Invalid license_type value of '{license_type}'. "
+ f"Available values: {LicenseType.keys()}"
+ )
+
+ r = self.__session.post(
+ url=f"{self.host}/{self.device_name}/get_license_challenge/{license_type}",
+ json={
+ "session_id": session_id.hex(),
+ "init_data": pssh.dumps(),
+ "privacy_mode": privacy_mode
+ }
+ ).json()
+ if r["status"] != 200:
+ raise ValueError(f"Cannot get Challenge, {r['message']} [{r['status']}]")
+ r = r["data"]
+
+ try:
+ challenge = base64.b64decode(r["challenge_b64"])
+ license_message = SignedMessage()
+ license_message.ParseFromString(challenge)
+ if license_message.SerializeToString() != challenge:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ raise InvalidLicenseMessage(f"Failed to parse license request, {e}")
+
+ return license_message.SerializeToString()
+
+ def parse_license(self, session_id: bytes, license_message: Union[SignedMessage, bytes, str]) -> None:
+ if not license_message:
+ raise InvalidLicenseMessage("Cannot parse an empty license_message")
+
+ if isinstance(license_message, str):
+ try:
+ license_message = base64.b64decode(license_message)
+ except (binascii.Error, binascii.Incomplete) as e:
+ raise InvalidLicenseMessage(f"Could not decode license_message as Base64, {e}")
+
+ if isinstance(license_message, bytes):
+ signed_message = SignedMessage()
+ try:
+ signed_message.ParseFromString(license_message)
+ if signed_message.SerializeToString() != license_message:
+ raise DecodeError("partial parse")
+ except DecodeError as e:
+ raise InvalidLicenseMessage(f"Could not parse license_message as a SignedMessage, {e}")
+ license_message = signed_message
+
+ if not isinstance(license_message, SignedMessage):
+ raise InvalidLicenseMessage(f"Expecting license_response to be a SignedMessage, got {license_message!r}")
+
+ if license_message.type != SignedMessage.MessageType.Value("LICENSE"):
+ raise InvalidLicenseMessage(
+ f"Expecting a LICENSE message, not a "
+ f"'{SignedMessage.MessageType.Name(license_message.type)}' message."
+ )
+
+ r = self.__session.post(
+ url=f"{self.host}/{self.device_name}/parse_license",
+ json={
+ "session_id": session_id.hex(),
+ "license_message": base64.b64encode(license_message.SerializeToString()).decode()
+ }
+ ).json()
+ if r["status"] != 200:
+ raise ValueError(f"Cannot parse License, {r['message']} [{r['status']}]")
+
+ def get_keys(self, session_id: bytes, type_: Optional[Union[int, str]] = None) -> list[Key]:
+ try:
+ if isinstance(type_, str):
+ License.KeyContainer.KeyType.Value(type_) # only test
+ elif isinstance(type_, int):
+ type_ = License.KeyContainer.KeyType.Name(type_)
+ elif type_ is None:
+ type_ = "ALL"
+ else:
+ raise TypeError(f"Expected type_ to be a {License.KeyContainer.KeyType} or int, not {type_!r}")
+ except ValueError as e:
+ raise ValueError(f"Could not parse type_ as a {License.KeyContainer.KeyType}, {e}")
+
+ r = self.__session.post(
+ url=f"{self.host}/{self.device_name}/get_keys/{type_}",
+ json={
+ "session_id": session_id.hex()
+ }
+ ).json()
+ if r["status"] != 200:
+ raise ValueError(f"Could not get {type_} Keys, {r['message']} [{r['status']}]")
+ r = r["data"]
+
+ return [
+ Key(
+ type_=key["type"],
+ kid=Key.kid_to_uuid(bytes.fromhex(key["key_id"])),
+ key=bytes.fromhex(key["key"]),
+ permissions=key["permissions"]
+ )
+ for key in r["keys"]
+ ]
+
+
+__all__ = ("RemoteCdm",)
diff --git a/pywidevine/serve.py b/pywidevine/serve.py
new file mode 100644
index 0000000..07b0602
--- /dev/null
+++ b/pywidevine/serve.py
@@ -0,0 +1,458 @@
+import base64
+import sys
+from pathlib import Path
+from typing import Any, Optional, Union
+
+from aiohttp.typedefs import Handler
+from google.protobuf.message import DecodeError
+
+from pywidevine.pssh import PSSH
+
+try:
+ from aiohttp import web
+except ImportError:
+ print(
+ "Missing the extra dependencies for serve functionality. "
+ "You may install them under poetry with `poetry install -E serve`, "
+ "or under pip with `pip install pywidevine[serve]`."
+ )
+ sys.exit(1)
+
+from pywidevine import __version__
+from pywidevine.cdm import Cdm
+from pywidevine.device import Device
+from pywidevine.exceptions import (InvalidContext, InvalidInitData, InvalidLicenseMessage, InvalidLicenseType,
+ InvalidSession, SignatureMismatch, TooManySessions)
+
+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({
+ "status": 200,
+ "message": "Pong!"
+ })
+
+
+@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({
+ "status": 403,
+ "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({
+ "status": 400,
+ "message": str(e)
+ }, status=400)
+
+ return web.json_response({
+ "status": 200,
+ "message": "Success",
+ "data": {
+ "session_id": session_id.hex(),
+ "device": {
+ "system_id": cdm.system_id,
+ "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({
+ "status": 400,
+ "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({
+ "status": 400,
+ "message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
+ }, status=400)
+
+ return web.json_response({
+ "status": 200,
+ "message": f"Successfully closed Session '{session_id.hex()}'."
+ })
+
+
+@routes.post("/{device}/set_service_certificate")
+async def set_service_certificate(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", "certificate"):
+ if required_field == "certificate":
+ has_field = required_field in body # it needs the key, but can be empty/null
+ else:
+ has_field = body.get(required_field)
+ if not has_field:
+ return web.json_response({
+ "status": 400,
+ "message": f"Missing required field '{required_field}' in JSON body."
+ }, status=400)
+
+ # get session id
+ session_id = bytes.fromhex(body["session_id"])
+
+ # get cdm
+ cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name))
+ if not cdm:
+ return web.json_response({
+ "status": 400,
+ "message": f"No Cdm session for {device_name} has been opened yet. No session to use."
+ }, status=400)
+
+ # set service certificate
+ certificate = body.get("certificate")
+ try:
+ provider_id = cdm.set_service_certificate(session_id, certificate)
+ except InvalidSession:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
+ }, status=400)
+ except DecodeError as e:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid Service Certificate, {e}"
+ }, status=400)
+ except SignatureMismatch:
+ return web.json_response({
+ "status": 400,
+ "message": "Signature Validation failed on the Service Certificate, rejecting."
+ }, status=400)
+
+ return web.json_response({
+ "status": 200,
+ "message": f"Successfully {['set', 'unset'][not certificate]} the Service Certificate.",
+ "data": {
+ "provider_id": provider_id
+ }
+ })
+
+
+@routes.post("/{device}/get_service_certificate")
+async def get_service_certificate(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({
+ "status": 400,
+ "message": f"Missing required field '{required_field}' in JSON body."
+ }, status=400)
+
+ # get session id
+ session_id = bytes.fromhex(body["session_id"])
+
+ # get cdm
+ cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name))
+ if not cdm:
+ return web.json_response({
+ "status": 400,
+ "message": f"No Cdm session for {device_name} has been opened yet. No session to use."
+ }, status=400)
+
+ # get service certificate
+ try:
+ service_certificate = cdm.get_service_certificate(session_id)
+ except InvalidSession:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
+ }, status=400)
+
+ if service_certificate:
+ service_certificate_b64 = base64.b64encode(service_certificate.SerializeToString()).decode()
+ else:
+ service_certificate_b64 = None
+
+ return web.json_response({
+ "status": 200,
+ "message": "Successfully got the Service Certificate.",
+ "data": {
+ "service_certificate": service_certificate_b64
+ }
+ })
+
+
+@routes.post("/{device}/get_license_challenge/{license_type}")
+async def get_license_challenge(request: web.Request) -> web.Response:
+ secret_key = request.headers["X-Secret-Key"]
+ device_name = request.match_info["device"]
+ license_type = request.match_info["license_type"]
+
+ body = await request.json()
+ for required_field in ("session_id", "init_data"):
+ if not body.get(required_field):
+ return web.json_response({
+ "status": 400,
+ "message": f"Missing required field '{required_field}' in JSON body."
+ }, status=400)
+
+ # get session id
+ session_id = bytes.fromhex(body["session_id"])
+
+ # get privacy mode flag
+ privacy_mode = body.get("privacy_mode", True)
+
+ # get cdm
+ cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name))
+ if not cdm:
+ return web.json_response({
+ "status": 400,
+ "message": f"No Cdm session for {device_name} has been opened yet. No session to use."
+ }, status=400)
+
+ # enforce service certificate (opt-in)
+ if request.app["config"].get("force_privacy_mode"):
+ privacy_mode = True
+ if not cdm.get_service_certificate(session_id):
+ return web.json_response({
+ "status": 403,
+ "message": "No Service Certificate set but Privacy Mode is Enforced."
+ }, status=403)
+
+ # get init data
+ init_data = PSSH(body["init_data"])
+
+ # get challenge
+ try:
+ license_request = cdm.get_license_challenge(
+ session_id=session_id,
+ pssh=init_data,
+ license_type=license_type,
+ privacy_mode=privacy_mode
+ )
+ except InvalidSession:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
+ }, status=400)
+ except InvalidInitData as e:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid Init Data, {e}"
+ }, status=400)
+ except InvalidLicenseType:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid License Type '{license_type}'"
+ }, status=400)
+
+ return web.json_response({
+ "status": 200,
+ "message": "Success",
+ "data": {
+ "challenge_b64": base64.b64encode(license_request).decode()
+ }
+ }, status=200)
+
+
+@routes.post("/{device}/parse_license")
+async def parse_license(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", "license_message"):
+ if not body.get(required_field):
+ return web.json_response({
+ "status": 400,
+ "message": f"Missing required field '{required_field}' in JSON body."
+ }, status=400)
+
+ # get session id
+ session_id = bytes.fromhex(body["session_id"])
+
+ # get cdm
+ cdm: Optional[Cdm] = request.app["cdms"].get((secret_key, device_name))
+ if not cdm:
+ return web.json_response({
+ "status": 400,
+ "message": f"No Cdm session for {device_name} has been opened yet. No session to use."
+ }, status=400)
+
+ # parse the license message
+ try:
+ cdm.parse_license(session_id, body["license_message"])
+ except InvalidSession:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
+ }, status=400)
+ except InvalidLicenseMessage as e:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid License Message, {e}"
+ }, status=400)
+ except InvalidContext as e:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid Context, {e}"
+ }, status=400)
+ except SignatureMismatch:
+ return web.json_response({
+ "status": 400,
+ "message": "Signature Validation failed on the License Message, rejecting."
+ }, status=400)
+
+ return web.json_response({
+ "status": 200,
+ "message": "Successfully parsed and loaded the Keys from the License message."
+ })
+
+
+@routes.post("/{device}/get_keys/{key_type}")
+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({
+ "status": 400,
+ "message": f"Missing required field '{required_field}' in JSON body."
+ }, status=400)
+
+ # get session id
+ session_id = bytes.fromhex(body["session_id"])
+
+ # get key type
+ key_type: Optional[str] = request.match_info["key_type"]
+ if key_type == "ALL":
+ key_type = None
+
+ # get cdm
+ cdm = request.app["cdms"].get((secret_key, device_name))
+ if not cdm:
+ return web.json_response({
+ "status": 400,
+ "message": f"No Cdm session for {device_name} has been opened yet. No session to use."
+ }, status=400)
+
+ # get keys
+ try:
+ keys = cdm.get_keys(session_id, key_type)
+ except InvalidSession:
+ return web.json_response({
+ "status": 400,
+ "message": f"Invalid Session ID '{session_id.hex()}', it may have expired."
+ }, status=400)
+ except ValueError as e:
+ return web.json_response({
+ "status": 400,
+ "message": f"The Key Type value '{key_type}' is invalid, {e}"
+ }, status=400)
+
+ # get the keys in json form
+ keys_json = [
+ {
+ "key_id": key.kid.hex,
+ "key": key.key.hex(),
+ "type": key.type,
+ "permissions": key.permissions,
+ }
+ for key in keys
+ if not key_type or key.type == key_type
+ ]
+
+ return web.json_response({
+ "status": 200,
+ "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({
+ "status": "401",
+ "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({
+ "status": "401",
+ "message": "Secret Key is Invalid, the Key is case-sensitive."
+ }, status=401)
+ else:
+ try:
+ response = await handler(request) # type: ignore[assignment]
+ except web.HTTPException as e:
+ request.app.logger.error(f"An unexpected error has occurred, {e}")
+ response = web.json_response({
+ "status": 500,
+ "message": e.reason
+ }, status=500)
+
+ response.headers.update({
+ "Server": f"https://github.com/devine-dl/pywidevine 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/pywidevine/session.py b/pywidevine/session.py
new file mode 100644
index 0000000..7cb816f
--- /dev/null
+++ b/pywidevine/session.py
@@ -0,0 +1,18 @@
+from typing import Optional
+
+from Crypto.Random import get_random_bytes
+
+from pywidevine.key import Key
+from pywidevine.license_protocol_pb2 import SignedDrmCertificate
+
+
+class Session:
+ def __init__(self, number: int):
+ self.number = number
+ self.id = get_random_bytes(16)
+ self.service_certificate: Optional[SignedDrmCertificate] = None
+ self.context: dict[bytes, tuple[bytes, bytes]] = {}
+ self.keys: list[Key] = []
+
+
+__all__ = ("Session",)
diff --git a/pywidevine/utils.py b/pywidevine/utils.py
new file mode 100644
index 0000000..6556d3e
--- /dev/null
+++ b/pywidevine/utils.py
@@ -0,0 +1,12 @@
+import shutil
+from pathlib import Path
+from typing import Optional
+
+
+def get_binary_path(*names: str) -> Optional[Path]:
+ """Get the path of the first found binary name."""
+ for name in names:
+ path = shutil.which(name)
+ if path:
+ return Path(path)
+ return None
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..d7c9931
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,8 @@
+protobuf
+pymp4==1.4.0
+pycryptodome
+click
+requests
+Unidecode
+PyYAML
+aiohttp
\ No newline at end of file
diff --git a/serve.example.yml b/serve.example.yml
new file mode 100644
index 0000000..83c99ae
--- /dev/null
+++ b/serve.example.yml
@@ -0,0 +1,20 @@
+# This data serves as an example configuration file for the `serve` command.
+# None of the sensitive data should be re-used.
+
+# List of Widevine Device (.wvd) file paths to use with serve.
+# Note: Each individual user needs explicit permission to use a device listed.
+devices:
+ - 'C:\Users\devine-dl\Documents\WVDs\test_device_001.wvd'
+
+# List of User's by Secret Key. The Secret Key must be supplied by the User to use the API.
+users:
+ fvYBh0C3fRAxlvyJcynD1see3GmNbIiC: # secret key, a-zA-Z-09{32} is recommended, case-sensitive
+ username: jane # only for internal logging, user will not see this name
+ devices: # list of allowed devices by filename
+ - test_key_001
+ # ...
+
+# All clients must provide a service certificate for privacy mode.
+# If the client does not provide a certificate, privacy mode may or may not be used.
+# Enforcing Privacy Mode helps protect the identity of the device and is recommended.
+force_privacy_mode: true