commit c62d708088ddbd4b31d5703cc9b7fa6ee17cb996 Author: Dmitry Isaenko Date: Wed Aug 10 15:55:50 2022 +0300 Init diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..e6edc53 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,36 @@ +kind: pipeline +type: docker +name: default + +steps: + - name: test + image: maven:3-jdk-11 + commands: + - mvn -B -DskipTests clean package + - mvn test -B + volumes: + - name: m2 + path: /root/.m2 + + - name: archive-artifact + image: alpine:latest + commands: + - mkdir -p /builds/libKonogonka + - cp target/libKonogonka-*-jar-with-dependencies.jar /builds/libKonogonka/ + volumes: + - name: builds + path: /builds + + - name: install-local-repo + image: alpine:latest + commands: + - mvn install:install-file -Dfile=./target/libKonogonka-*-jar-with-dependencies.jar -DgroupId=ru.redrise -DartifactId=libKonogonka -Dversion=`grep -m 1 '' pom.xml| sed -e 's/\s*.\/\?version>//g' + ` -Dpackaging=jar -DgeneratePom=true; + +volumes: + - name: m2 + host: + path: /home/docker/drone/files/m2 + - name: builds + host: + path: /home/www/builds \ No newline at end of file diff --git a/HOWTO.txt b/HOWTO.txt new file mode 100644 index 0000000..f093d27 --- /dev/null +++ b/HOWTO.txt @@ -0,0 +1,9 @@ +How to add library into the local maven repository: + +mvn install:install-file \ + -Dfile=./libKonogonka/target/libKonogonka-0.1-jar-with-dependencies.jar \ + -DgroupId=ru.redrise \ + -DartifactId=libKonogonka \ + -Dversion=0.1 \ + -Dpackaging=jar \ + -DgeneratePom=true \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9cecc1d --- /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. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + 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: + + {project} Copyright (C) {year} {fullname} + 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..f1e03c7 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# konogonka + +[![Build Status](https://ci.redrise.ru/api/badges/desu/libKonogonka/status.svg)](https://ci.redrise.ru/desu/libKonogonka) + +Library to work with NS-specific files / filesystem images. Dedicated back end part of [konogonka](https://github.com/developersu/konogonka) ([independent src location](https://git.redrise.ru/desu/konogonka)) + +### License + +[GNU General Public License v3+](https://git.redrise.ru/desu/libKonogonka/LICENSE) + +### Used libraries & resources +* [Bouncy Castle](https://www.bouncycastle.org/) for Java. +* [Java-XTS-AES](https://github.com/horrorho/Java-XTS-AES) by horrorho with minimal changes. + +#### Thanks +* Switch brew wiki +* Original ScriesM software +* roothorick, [shchmue](https://github.com/shchmue/), He, other Team AtlasNX discord members for their advices, notes and examples! + +### System requirements + +JRE/JDK 8u60 or higher. + +### Build + +See .drone.yml \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5ffc26f --- /dev/null +++ b/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + ru.redrise + libKonogonka + 0.1 + + https://git.redrise.ru/desu/${project.name}}/ + + NS filesystem library + + 2022 + + Dmitry Isaenko + https://developersu.blogspot.com/ + + + + + GPLv3 + LICENSE + manual + + + + + + developer.su + Dmitry Isaenko + + Developer + + +3 + https://developersu.blogspot.com/ + + + + + UTF-8 + yyyyMMdd.HHmmss + + + + GitHub + https://github.com/developer_su/${project.artifactId}/issues + + + + + + org.bouncycastle + bcprov-jdk15on + 1.70 + compile + + + net.jcip + jcip-annotations + 1.0 + compile + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.0 + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.10.1 + + 8 + 8 + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + + nsusbloader.Main + + + + jar-with-dependencies + + + + + + make-assembly + package + + single + + + + + + + \ No newline at end of file diff --git a/src/main/java/libKonogonka/LoperConverter.java b/src/main/java/libKonogonka/LoperConverter.java new file mode 100644 index 0000000..6618114 --- /dev/null +++ b/src/main/java/libKonogonka/LoperConverter.java @@ -0,0 +1,64 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class LoperConverter { + public static int getLEint(byte[] bytes, int fromOffset){ + return ByteBuffer.wrap(bytes, fromOffset, 0x4).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + public static long getLElong(byte[] bytes, int fromOffset){ + return ByteBuffer.wrap(bytes, fromOffset, 0x8).order(ByteOrder.LITTLE_ENDIAN).getLong(); + } + /** + * Convert int to long. Workaround to store unsigned int + * @param bytes original array + * @param fromOffset start position of the 4-bytes value + * */ + public static long getLElongOfInt(byte[] bytes, int fromOffset){ + final byte[] holder = new byte[8]; + System.arraycopy(bytes, fromOffset, holder, 0, 4); + return ByteBuffer.wrap(holder).order(ByteOrder.LITTLE_ENDIAN).getLong(); + } + + public static String byteArrToHexString(byte[] bArr){ + if (bArr == null) + return ""; + StringBuilder sb = new StringBuilder(); + for (byte b: bArr) + sb.append(String.format("%02x", b)); + return sb.toString(); + } + + public static String longToOctString(long value){ + return String.format("%64s", Long.toBinaryString( value )).replace(' ', '0'); + } + + public static byte[] flip(byte[] bytes){ + int size = bytes.length; + byte[] ret = new byte[size]; + for (int i = 0; i < size; i++){ + ret[size-i-1] = bytes[i]; + } + return ret; + } +} diff --git a/src/main/java/libKonogonka/RainbowDump.java b/src/main/java/libKonogonka/RainbowDump.java new file mode 100644 index 0000000..fbd7ad6 --- /dev/null +++ b/src/main/java/libKonogonka/RainbowDump.java @@ -0,0 +1,62 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka; + +import java.nio.charset.StandardCharsets; + +/** + * Debug tool like hexdump <3 + */ +public class RainbowDump { + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_BLACK = "\u001B[30m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_YELLOW = "\u001B[33m"; + private static final String ANSI_BLUE = "\u001B[34m"; + private static final String ANSI_PURPLE = "\u001B[35m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_WHITE = "\u001B[37m"; + + + public static void hexDumpUTF8(byte[] byteArray){ + if (byteArray == null || byteArray.length == 0) + return; + System.out.print(ANSI_BLUE); + for (int i=0; i < byteArray.length; i++) + System.out.print(String.format("%02d-", i%100)); + System.out.println(">"+ANSI_RED+byteArray.length+ANSI_RESET); + for (byte b: byteArray) + System.out.print(String.format("%02x ", b)); + System.out.println(); + System.out.print(new String(byteArray, StandardCharsets.UTF_8)+"\n"); + } + + public static void octDumpInt(int value){ + System.out.println(String.format("%32s", Integer.toBinaryString( value )).replace(' ', '0')+" | "+value); + } + + public static void octDumpLong(long value){ + System.out.println(String.format("%64s", Long.toBinaryString( value )).replace(' ', '0')+" | "+value); + } + + public static String formatDecHexString(long value){ + return String.format("%-20d 0x%x", value, value); + } +} diff --git a/src/main/java/libKonogonka/Tools/ASuperInFileProvider.java b/src/main/java/libKonogonka/Tools/ASuperInFileProvider.java new file mode 100644 index 0000000..4be1087 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/ASuperInFileProvider.java @@ -0,0 +1,42 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools; + +import java.io.IOException; +import java.io.PipedInputStream; + +/** + * Any class of this type must be able to accept data from stream (and file as any other). + * */ + +public abstract class ASuperInFileProvider { + protected byte[] readFromStream(PipedInputStream pis, int size) throws IOException { + byte[] buffer = new byte[size]; + int startingPos = 0; + int readCnt; + while (size > 0){ + readCnt = pis.read(buffer, startingPos, size); + if (readCnt == -1) + return null; + startingPos += readCnt; + size -= readCnt; + } + return buffer; + } +} diff --git a/src/main/java/libKonogonka/Tools/ISuperProvider.java b/src/main/java/libKonogonka/Tools/ISuperProvider.java new file mode 100644 index 0000000..3118841 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/ISuperProvider.java @@ -0,0 +1,32 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools; + +import java.io.File; +import java.io.PipedInputStream; +/** + * Any class of this type must provide streams + * */ +public interface ISuperProvider { + PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception; + PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception; + + File getFile(); + long getRawFileDataStart(); +} diff --git a/src/main/java/libKonogonka/Tools/NCA/NCAContent.java b/src/main/java/libKonogonka/Tools/NCA/NCAContent.java new file mode 100644 index 0000000..0ebf6df --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NCA/NCAContent.java @@ -0,0 +1,443 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NCA; + +import libKonogonka.LoperConverter; +import libKonogonka.Tools.NCA.NCASectionTableBlock.NCASectionBlock; +import libKonogonka.Tools.PFS0.IPFS0Provider; +import libKonogonka.Tools.PFS0.PFS0EncryptedProvider; +import libKonogonka.Tools.PFS0.PFS0Provider; +import libKonogonka.Tools.RomFs.IRomFsProvider; +import libKonogonka.Tools.RomFs.RomFsEncryptedProvider; +import libKonogonka.ctraes.AesCtrDecryptSimple; +import libKonogonka.exceptions.EmptySectionException; + +import java.io.*; +import java.util.LinkedList; +/** + * THIS CLASS BECOMES MORE UGLY AFTER EACH ITERATION OF REFACTORING. + * TODO: MAKE SOME DECOMPOSITION + * */ +public class NCAContent { + private File file; + private long offsetPosition; + private NCASectionBlock ncaSectionBlock; + private NCAHeaderTableEntry ncaHeaderTableEntry; + private byte[] decryptedKey; + + private LinkedList Pfs0SHA256hashes; + private IPFS0Provider pfs0; + private IRomFsProvider romfs; + + // TODO: if decryptedKey is empty, throw exception ?? + public NCAContent(File file, + long offsetPosition, + NCASectionBlock ncaSectionBlock, + NCAHeaderTableEntry ncaHeaderTableEntry, + byte[] decryptedKey) throws Exception + { + this.file = file; + this.offsetPosition = offsetPosition; + this.ncaSectionBlock = ncaSectionBlock; + this.ncaHeaderTableEntry = ncaHeaderTableEntry; + this.decryptedKey = decryptedKey; + + Pfs0SHA256hashes = new LinkedList<>(); + // If nothing to do + if (ncaHeaderTableEntry.getMediaEndOffset() == 0) + throw new EmptySectionException("Empty section"); + // If it's PFS0Provider + if (ncaSectionBlock.getSuperBlockPFS0() != null) + this.proceedPFS0(); + else if (ncaSectionBlock.getSuperBlockIVFC() != null) + this.proceedRomFs(); + else + throw new Exception("NCAContent(): Not supported. PFS0 or RomFS supported only."); + } + + private void proceedPFS0() throws Exception { + switch (ncaSectionBlock.getCryptoType()){ + case 0x01: + proceedPFS0NotEncrypted(); // IF NO ENCRYPTION + break; + case 0x03: + proceedPFS0Encrypted(); // If encrypted regular [ 0x03 ] + break; + default: + throw new Exception("NCAContent() -> proceedPFS0(): Non-supported 'Crypto type'"); + } + } + private void proceedPFS0NotEncrypted() throws Exception{ + RandomAccessFile raf = new RandomAccessFile(file, "r"); + long thisMediaLocation = offsetPosition + (ncaHeaderTableEntry.getMediaStartOffset() * 0x200); + long hashTableLocation = thisMediaLocation + ncaSectionBlock.getSuperBlockPFS0().getHashTableOffset(); + long pfs0Location = thisMediaLocation + ncaSectionBlock.getSuperBlockPFS0().getPfs0offset(); + + raf.seek(hashTableLocation); + + byte[] rawData; + long sha256recordsNumber = ncaSectionBlock.getSuperBlockPFS0().getHashTableSize() / 0x20; + // Collect hashes + for (int i = 0; i < sha256recordsNumber; i++){ + rawData = new byte[0x20]; // 32 bytes - size of SHA256 hash + if (raf.read(rawData) != -1) + Pfs0SHA256hashes.add(rawData); + else { + raf.close(); + return; // TODO: fix + } + } + raf.close(); + // Get pfs0 + pfs0 = new PFS0Provider(file, pfs0Location); + } + private void proceedPFS0Encrypted() throws Exception{ + new CryptoSection03Pfs0(file, + offsetPosition, + decryptedKey, + ncaSectionBlock, + ncaHeaderTableEntry.getMediaStartOffset(), + ncaHeaderTableEntry.getMediaEndOffset()); + } + + private void proceedRomFs() throws Exception{ + switch (ncaSectionBlock.getCryptoType()){ + case 0x01: + proceedRomFsNotEncrypted(); // IF NO ENCRYPTION + break; + case 0x03: + proceedRomFsEncrypted(); // If encrypted regular [ 0x03 ] + break; + default: + throw new Exception("NCAContent() -> proceedRomFs(): Non-supported 'Crypto type'"); + } + } + private void proceedRomFsNotEncrypted(){ + // TODO: Clarify, implement if needed + System.out.println("proceedRomFs() -> proceedRomFsNotEncrypted() is not implemented :("); + } + private void proceedRomFsEncrypted() throws Exception{ + if (decryptedKey == null) + throw new Exception("CryptoSection03: unable to proceed. No decrypted key provided."); + + this.romfs = new RomFsEncryptedProvider( + offsetPosition, + ncaSectionBlock.getSuperBlockIVFC().getLvl6Offset(), + file, + decryptedKey, + ncaSectionBlock.getSectionCTR(), + ncaHeaderTableEntry.getMediaStartOffset(), + ncaHeaderTableEntry.getMediaEndOffset()); + } + + public LinkedList getPfs0SHA256hashes() { return Pfs0SHA256hashes; } + public IPFS0Provider getPfs0() { return pfs0; } + public IRomFsProvider getRomfs() { return romfs; } + + private class CryptoSection03Pfs0 { + + CryptoSection03Pfs0(File file, + long offsetPosition, + byte[] decryptedKey, + NCASectionBlock ncaSectionBlock, + long mediaStartBlocksOffset, + long mediaEndBlocksOffset) throws Exception + { + /*//-------------------------------------------------------------------------------------------------- + System.out.println("Media start location: " + mediaStartBlocksOffset); + System.out.println("Media end location: " + mediaEndBlocksOffset); + System.out.println("Media size : " + (mediaEndBlocksOffset-mediaStartBlocksOffset)); + System.out.println("Media act. location: " + (offsetPosition + (mediaStartBlocksOffset * 0x200))); + System.out.println("SHA256 hash tbl size: " + ncaSectionBlock.getSuperBlockPFS0().getHashTableSize()); + System.out.println("SHA256 hash tbl offs: " + ncaSectionBlock.getSuperBlockPFS0().getHashTableOffset()); + System.out.println("PFS0 Offs: " + ncaSectionBlock.getSuperBlockPFS0().getPfs0offset()); + System.out.println("SHA256 records: " + (ncaSectionBlock.getSuperBlockPFS0().getHashTableSize() / 0x20)); + System.out.println("KEY: " + LoperConverter.byteArrToHexString(decryptedKey)); + System.out.println("CTR: " + LoperConverter.byteArrToHexString(ncaSectionBlock.getSectionCTR())); + System.out.println(); + //--------------------------------------------------------------------------------------------------*/ + if (decryptedKey == null) + throw new Exception("CryptoSection03: unable to proceed. No decrypted key provided."); + + RandomAccessFile raf = new RandomAccessFile(file, "r"); + long abosluteOffsetPosition = offsetPosition + (mediaStartBlocksOffset * 0x200); + raf.seek(abosluteOffsetPosition); + + AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(decryptedKey, ncaSectionBlock.getSectionCTR(), mediaStartBlocksOffset * 0x200); + + byte[] encryptedBlock; + byte[] dectyptedBlock; + long mediaBlocksSize = mediaEndBlocksOffset - mediaStartBlocksOffset; + // Prepare thread to parse encrypted data + PipedOutputStream streamOut = new PipedOutputStream(); + PipedInputStream streamInp = new PipedInputStream(streamOut); + + Thread pThread = new Thread(new ParseThread( + streamInp, + ncaSectionBlock.getSuperBlockPFS0().getPfs0offset(), + ncaSectionBlock.getSuperBlockPFS0().getHashTableOffset(), + ncaSectionBlock.getSuperBlockPFS0().getHashTableSize(), + offsetPosition, + file, + decryptedKey, + ncaSectionBlock.getSectionCTR(), + mediaStartBlocksOffset, + mediaEndBlocksOffset + )); + pThread.start(); + // Decrypt data + for (int i = 0; i < mediaBlocksSize; i++){ + encryptedBlock = new byte[0x200]; + if (raf.read(encryptedBlock) != -1){ + //dectyptedBlock = aesCtr.decrypt(encryptedBlock); + dectyptedBlock = decryptor.dectyptNext(encryptedBlock); + // Writing decrypted data to pipe + try { + streamOut.write(dectyptedBlock); + } + catch (IOException e){ + break; + } + } + } + pThread.join(); + streamOut.close(); + raf.close(); + } + /* + * Since we representing decrypted data as stream (it's easier to look on it this way), + * this thread will be parsing it. + * */ + private class ParseThread implements Runnable{ + + PipedInputStream pipedInputStream; + + long hashTableOffset; + long hashTableSize; + long hashTableRecordsCount; + long pfs0offset; + + private long MetaOffsetPositionInFile; + private File MetaFileWithEncPFS0; + private byte[] MetaKey; + private byte[] MetaSectionCTR; + private long MetaMediaStartOffset; + private long MetaMediaEndOffset; + + + ParseThread(PipedInputStream pipedInputStream, + long pfs0offset, + long hashTableOffset, + long hashTableSize, + + long MetaOffsetPositionInFile, + File MetaFileWithEncPFS0, + byte[] MetaKey, + byte[] MetaSectionCTR, + long MetaMediaStartOffset, + long MetaMediaEndOffset + ){ + this.pipedInputStream = pipedInputStream; + this.hashTableOffset = hashTableOffset; + this.hashTableSize = hashTableSize; + this.hashTableRecordsCount = hashTableSize / 0x20; + this.pfs0offset = pfs0offset; + + this.MetaOffsetPositionInFile = MetaOffsetPositionInFile; + this.MetaFileWithEncPFS0 = MetaFileWithEncPFS0; + this.MetaKey = MetaKey; + this.MetaSectionCTR = MetaSectionCTR; + this.MetaMediaStartOffset = MetaMediaStartOffset; + this.MetaMediaEndOffset = MetaMediaEndOffset; + + } + + @Override + public void run() { + long counter = 0; // How many bytes already read + + try{ + if (hashTableOffset > 0){ + if (hashTableOffset != pipedInputStream.skip(hashTableOffset)) + return; // TODO: fix? + counter = hashTableOffset; + } + // Loop for collecting all recrods from sha256 hash table + while ((counter - hashTableOffset) < hashTableSize){ + int hashCounter = 0; + byte[] sectionHash = new byte[0x20]; + // Loop for collecting bytes for every SINGLE records, where record size == 0x20 + while (hashCounter < 0x20){ + int currentByte = pipedInputStream.read(); + if (currentByte == -1) + break; + sectionHash[hashCounter] = (byte)currentByte; + hashCounter++; + counter++; + } + // Write after collecting + Pfs0SHA256hashes.add(sectionHash); // From the NCAContentProvider obviously + } + // Skip padding and go to PFS0 location + if (counter < pfs0offset){ + long toSkip = pfs0offset-counter; + if (toSkip != pipedInputStream.skip(toSkip)) + return; // TODO: fix? + counter += toSkip; + } + //--------------------------------------------------------- + pfs0 = new PFS0EncryptedProvider(pipedInputStream, + counter, + MetaOffsetPositionInFile, + MetaFileWithEncPFS0, + MetaKey, + MetaSectionCTR, + MetaMediaStartOffset, + MetaMediaEndOffset); + pipedInputStream.close(); + } + catch (Exception e){ + System.out.println("'ParseThread' thread exception"); + e.printStackTrace(); + } + finally { + System.out.println("Thread dies"); + } + } + } + } + + /** + * Export NCA content AS IS. + * Not so good for PFS0 since there are SHAs list that discourages but good for 'romfs' and things like that + * */ + public PipedInputStream getRawDataContentPipedInpStream() throws Exception { + long mediaStartBlocksOffset = ncaHeaderTableEntry.getMediaStartOffset(); + long mediaEndBlocksOffset = ncaHeaderTableEntry.getMediaEndOffset(); + long mediaBlocksSize = mediaEndBlocksOffset - mediaStartBlocksOffset; + + RandomAccessFile raf = new RandomAccessFile(file, "r"); + ///-------------------------------------------------------------------------------------------------- + System.out.println("NCAContent() -> exportEncryptedSectionType03() Debug information"); + System.out.println("Media start location: " + mediaStartBlocksOffset); + System.out.println("Media end location: " + mediaEndBlocksOffset); + System.out.println("Media size : " + (mediaEndBlocksOffset-mediaStartBlocksOffset)); + System.out.println("Media act. location: " + (offsetPosition + (mediaStartBlocksOffset * 0x200))); + System.out.println("KEY: " + LoperConverter.byteArrToHexString(decryptedKey)); + System.out.println("CTR: " + LoperConverter.byteArrToHexString(ncaSectionBlock.getSectionCTR())); + System.out.println(); + //---------------------------------------------------------------------------------------------------/ + + if (ncaSectionBlock.getCryptoType() == 0x01){ + System.out.println("NCAContent -> getRawDataContentPipedInpStream (Zero encryption section type 01): Thread started"); + + Thread workerThread; + PipedOutputStream streamOut = new PipedOutputStream(); + + PipedInputStream streamIn = new PipedInputStream(streamOut); + workerThread = new Thread(() -> { + try { + byte[] rawDataBlock; + for (int i = 0; i < mediaBlocksSize; i++){ + rawDataBlock = new byte[0x200]; + if (raf.read(rawDataBlock) != -1) + streamOut.write(rawDataBlock); + else + break; + } + } + catch (Exception e){ + System.out.println("NCAContent -> exportRawData(): "+e.getMessage()); + e.printStackTrace(); + } + finally { + try { + raf.close(); + }catch (Exception ignored) {} + try { + streamOut.close(); + }catch (Exception ignored) {} + } + System.out.println("NCAContent -> exportRawData(): Thread died"); + }); + workerThread.start(); + return streamIn; + } + else if (ncaSectionBlock.getCryptoType() == 0x03){ + System.out.println("NCAContent -> getRawDataContentPipedInpStream (Encrypted Section Type 03): Thread started"); + + if (decryptedKey == null) + throw new Exception("NCAContent -> exportRawData(): unable to proceed. No decrypted key provided."); + + Thread workerThread; + PipedOutputStream streamOut = new PipedOutputStream(); + + PipedInputStream streamIn = new PipedInputStream(streamOut); + workerThread = new Thread(() -> { + try { + //RandomAccessFile raf = new RandomAccessFile(file, "r"); + long abosluteOffsetPosition = offsetPosition + (mediaStartBlocksOffset * 0x200); + raf.seek(abosluteOffsetPosition); + + AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(decryptedKey, + ncaSectionBlock.getSectionCTR(), + mediaStartBlocksOffset * 0x200); + + byte[] encryptedBlock; + byte[] dectyptedBlock; + + // Decrypt data + for (int i = 0; i < mediaBlocksSize; i++){ + encryptedBlock = new byte[0x200]; + if (raf.read(encryptedBlock) != -1){ + dectyptedBlock = decryptor.dectyptNext(encryptedBlock); + // Writing decrypted data to pipe + streamOut.write(dectyptedBlock); + } + else + break; + } + } + catch (Exception e){ + System.out.println("NCAContent -> exportRawData(): "+e.getMessage()); + e.printStackTrace(); + } + finally { + try { + raf.close(); + }catch (Exception ignored) {} + try { + streamOut.close(); + }catch (Exception ignored) {} + } + System.out.println("NCAContent -> exportRawData(): Thread died"); + }); + workerThread.start(); + return streamIn; + } + else + return null; + } + public long getRawDataContentSize(){ + return (ncaHeaderTableEntry.getMediaEndOffset() - ncaHeaderTableEntry.getMediaStartOffset()) * 0x200; + } + public String getFileName(){ + return file.getName(); + } +} \ No newline at end of file diff --git a/src/main/java/libKonogonka/Tools/NCA/NCAHeaderTableEntry.java b/src/main/java/libKonogonka/Tools/NCA/NCAHeaderTableEntry.java new file mode 100644 index 0000000..ab697ef --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NCA/NCAHeaderTableEntry.java @@ -0,0 +1,53 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NCA; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +public class NCAHeaderTableEntry { + + private long mediaStartOffset; + private long mediaEndOffset; + private byte[] unknwn1; + private byte[] unknwn2; + + public NCAHeaderTableEntry(byte[] table) throws Exception{ + if (table.length < 0x10) + throw new Exception("Section Table size is too small."); + + this.mediaStartOffset = convertUnsignedIntBytesToLong(Arrays.copyOfRange(table, 0x0, 0x4)); + this.mediaEndOffset = convertUnsignedIntBytesToLong(Arrays.copyOfRange(table, 0x4, 0x8)); + this.unknwn1 = Arrays.copyOfRange(table, 0x8, 0xC); + this.unknwn2 = Arrays.copyOfRange(table, 0xC, 0x10); + } + + private long convertUnsignedIntBytesToLong(byte[] intBytes){ + if (intBytes.length == 4) + return ByteBuffer.wrap(Arrays.copyOf(intBytes, 8)).order(ByteOrder.LITTLE_ENDIAN).getLong(); + else + return -1; + } + + public long getMediaStartOffset() { return mediaStartOffset; } + public long getMediaEndOffset() { return mediaEndOffset; } + public byte[] getUnknwn1() { return unknwn1; } + public byte[] getUnknwn2() { return unknwn2; } +} \ No newline at end of file diff --git a/src/main/java/libKonogonka/Tools/NCA/NCAProvider.java b/src/main/java/libKonogonka/Tools/NCA/NCAProvider.java new file mode 100644 index 0000000..3b6ec8f --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NCA/NCAProvider.java @@ -0,0 +1,385 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NCA; + +import libKonogonka.Tools.NCA.NCASectionTableBlock.NCASectionBlock; +import libKonogonka.exceptions.EmptySectionException; +import libKonogonka.xtsaes.XTSAESCipher; +import org.bouncycastle.crypto.params.KeyParameter; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.io.File; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; + +import static libKonogonka.LoperConverter.byteArrToHexString; +import static libKonogonka.LoperConverter.getLElong; + +// TODO: check file size +public class NCAProvider { + private File file; // File that contains NCA + private long offset; // Offset where NCA actually located + private HashMap keys; // hashmap with keys using _0x naming (where x number 0-N) + // Header + private byte[] rsa2048one; + private byte[] rsa2048two; + private String magicnum; + private byte systemOrGcIndicator; + private byte contentType; + private byte cryptoType1; // keyblob index. Considering as number within application/ocean/system + private byte keyIndex; // application/ocean/system (kaek index?) + private long ncaSize; // Size of this NCA (bytes) + private byte[] titleId; + private byte[] contentIndx; + private byte[] sdkVersion; // version ver_revision.ver_micro.vev_minor.ver_major + private byte cryptoType2; // keyblob index. Considering as number within application/ocean/system | AKA KeyGeneration + private byte Header1SignatureKeyGeneration; + private byte[] keyGenerationReserved; + private byte[] rightsId; + + private byte cryptoTypeReal; + + private byte[] sha256hash0; + private byte[] sha256hash1; + private byte[] sha256hash2; + private byte[] sha256hash3; + + private byte[] encryptedKey0; + private byte[] encryptedKey1; + private byte[] encryptedKey2; + private byte[] encryptedKey3; + + private byte[] decryptedKey0; + private byte[] decryptedKey1; + private byte[] decryptedKey2; + private byte[] decryptedKey3; + + private NCAHeaderTableEntry tableEntry0; + private NCAHeaderTableEntry tableEntry1; + private NCAHeaderTableEntry tableEntry2; + private NCAHeaderTableEntry tableEntry3; + + private NCASectionBlock sectionBlock0; + private NCASectionBlock sectionBlock1; + private NCASectionBlock sectionBlock2; + private NCASectionBlock sectionBlock3; + + private NCAContent ncaContent0; + private NCAContent ncaContent1; + private NCAContent ncaContent2; + private NCAContent ncaContent3; + + public NCAProvider(File file, HashMap keys) throws Exception{ + this(file, keys, 0); + } + + public NCAProvider (File file, HashMap keys, long offsetPosition) throws Exception{ + this.keys = keys; + String header_key = keys.get("header_key"); + if (header_key == null ) + throw new Exception("header_key is not found within key set provided."); + if (header_key.length() != 64) + throw new Exception("header_key is too small or too big. Must be 64 symbols."); + + this.file = file; + this.offset = offsetPosition; + + KeyParameter key1 = new KeyParameter( + hexStrToByteArray(header_key.substring(0, 32)) + ); + KeyParameter key2 = new KeyParameter( + hexStrToByteArray(header_key.substring(32, 64)) + ); + + XTSAESCipher xtsaesCipher = new XTSAESCipher(false); + xtsaesCipher.init(false, key1, key2); + //------------------------------------------------------------------------------------------------------------------------- + byte[] decryptedHeader = new byte[0xC00]; + + RandomAccessFile raf = new RandomAccessFile(file, "r"); + byte[] encryptedSequence = new byte[0x200]; + byte[] decryptedSequence; + + raf.seek(offsetPosition); + + for (int i = 0; i < 6; i++){ + if (raf.read(encryptedSequence) != 0x200) + throw new Exception("Read error "+i); + decryptedSequence = new byte[0x200]; + xtsaesCipher.processDataUnit(encryptedSequence, 0, 0x200, decryptedSequence, 0, i); + System.arraycopy(decryptedSequence, 0, decryptedHeader, i * 0x200, 0x200); + } + + getHeader(decryptedHeader); + + raf.close(); + + getNCAContent(); + /* + //--------------------------------------------------------------------- + FileInputStream fis = new FileInputStream(file); + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("/tmp/decrypted.nca")); + int i = 0; + byte[] block = new byte[0x200]; + while (fis.read(block) != -1){ + decryptedSequence = new byte[0x200]; + xtsaesCipher.processDataUnit(block, 0, 0x200, decryptedSequence, 0, i++); + bos.write(decryptedSequence); + } + bos.close(); + //---------------------------------------------------------------------*/ + } + + private byte[] hexStrToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + private void getHeader(byte[] decryptedData) throws Exception{ + rsa2048one = Arrays.copyOfRange(decryptedData, 0, 0x100); + rsa2048two = Arrays.copyOfRange(decryptedData, 0x100, 0x200); + magicnum = new String(decryptedData, 0x200, 0x4, StandardCharsets.US_ASCII); + systemOrGcIndicator = decryptedData[0x204]; + contentType = decryptedData[0x205]; + cryptoType1 = decryptedData[0x206]; + keyIndex = decryptedData[0x207]; + ncaSize = getLElong(decryptedData, 0x208); + titleId = Arrays.copyOfRange(decryptedData, 0x210, 0x218); + contentIndx = Arrays.copyOfRange(decryptedData, 0x218, 0x21C); + sdkVersion = Arrays.copyOfRange(decryptedData, 0x21c, 0x220); + cryptoType2 = decryptedData[0x220]; + Header1SignatureKeyGeneration = decryptedData[0x221]; + keyGenerationReserved = Arrays.copyOfRange(decryptedData, 0x222, 0x230); + rightsId = Arrays.copyOfRange(decryptedData, 0x230, 0x240); + byte[] tableBytes = Arrays.copyOfRange(decryptedData, 0x240, 0x280); + byte[] sha256tableBytes = Arrays.copyOfRange(decryptedData, 0x280, 0x300); + sha256hash0 = Arrays.copyOfRange(sha256tableBytes, 0, 0x20); + sha256hash1 = Arrays.copyOfRange(sha256tableBytes, 0x20, 0x40); + sha256hash2 = Arrays.copyOfRange(sha256tableBytes, 0x40, 0x60); + sha256hash3 = Arrays.copyOfRange(sha256tableBytes, 0x60, 0x80); + byte [] encryptedKeysArea = Arrays.copyOfRange(decryptedData, 0x300, 0x340); + + encryptedKey0 = Arrays.copyOfRange(encryptedKeysArea, 0, 0x10); + encryptedKey1 = Arrays.copyOfRange(encryptedKeysArea, 0x10, 0x20); + encryptedKey2 = Arrays.copyOfRange(encryptedKeysArea, 0x20, 0x30); + encryptedKey3 = Arrays.copyOfRange(encryptedKeysArea, 0x30, 0x40); + + // Calculate real Crypto Type + if (cryptoType1 < cryptoType2) + cryptoTypeReal = cryptoType2; + else + cryptoTypeReal = cryptoType1; + + if (cryptoTypeReal > 0) // TODO: CLARIFY WHY THE FUCK IS IT FAIR???? + cryptoTypeReal -= 1; + + //todo: if nca3 proceed + // Decrypt keys if encrypted + if (Arrays.equals(rightsId, new byte[0x10])) { + String keyAreaKey; + switch (keyIndex){ + case 0: + keyAreaKey = keys.get(String.format("key_area_key_application_%02x", cryptoTypeReal)); + break; + case 1: + keyAreaKey = keys.get(String.format("key_area_key_ocean_%02x", cryptoTypeReal)); + break; + case 2: + keyAreaKey = keys.get(String.format("key_area_key_system_%02x", cryptoTypeReal)); + break; + default: + keyAreaKey = null; + } + + if (keyAreaKey != null){ + SecretKeySpec skSpec = new SecretKeySpec(hexStrToByteArray(keyAreaKey), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, skSpec); + decryptedKey0 = cipher.doFinal(encryptedKey0); + decryptedKey1 = cipher.doFinal(encryptedKey1); + decryptedKey2 = cipher.doFinal(encryptedKey2); + decryptedKey3 = cipher.doFinal(encryptedKey3); + } + else + keyAreaKeyNotSupportedOrFound(); + } + + tableEntry0 = new NCAHeaderTableEntry(tableBytes); + tableEntry1 = new NCAHeaderTableEntry(Arrays.copyOfRange(tableBytes, 0x10, 0x20)); + tableEntry2 = new NCAHeaderTableEntry(Arrays.copyOfRange(tableBytes, 0x20, 0x30)); + tableEntry3 = new NCAHeaderTableEntry(Arrays.copyOfRange(tableBytes, 0x30, 0x40)); + + sectionBlock0 = new NCASectionBlock(Arrays.copyOfRange(decryptedData, 0x400, 0x600)); + sectionBlock1 = new NCASectionBlock(Arrays.copyOfRange(decryptedData, 0x600, 0x800)); + sectionBlock2 = new NCASectionBlock(Arrays.copyOfRange(decryptedData, 0x800, 0xa00)); + sectionBlock3 = new NCASectionBlock(Arrays.copyOfRange(decryptedData, 0xa00, 0xc00)); + } + + private void keyAreaKeyNotSupportedOrFound() throws Exception{ + StringBuilder exceptionStringBuilder = new StringBuilder("key_area_key_"); + switch (keyIndex){ + case 0: + exceptionStringBuilder.append("application_"); + break; + case 1: + exceptionStringBuilder.append("ocean_"); + break; + case 2: + exceptionStringBuilder.append("system_"); + break; + default: + exceptionStringBuilder.append(keyIndex); + exceptionStringBuilder.append("[UNKNOWN]_"); + } + exceptionStringBuilder.append(String.format("%02x", cryptoTypeReal)); + exceptionStringBuilder.append(" requested. Not supported or not found."); + throw new Exception(exceptionStringBuilder.toString()); + } + + private void getNCAContent(){ + byte[] key; + + // If empty Rights ID + if (Arrays.equals(rightsId, new byte[0x10])) { + key = decryptedKey2; // TODO: Just remember this dumb hack + } + else { + try { + byte[] rightsIDkey = hexStrToByteArray(keys.get(byteArrToHexString(rightsId))); // throws NullPointerException + + SecretKeySpec skSpec = new SecretKeySpec( + hexStrToByteArray(keys.get(String.format("titlekek_%02x", cryptoTypeReal)) + ), "AES"); + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, skSpec); + key = cipher.doFinal(rightsIDkey); + } + catch (Exception e){ + e.printStackTrace(); + System.out.println("No title.keys loaded?"); + return; + } + } + try { + this.ncaContent0 = new NCAContent(file, offset, sectionBlock0, tableEntry0, key); + } + catch (EmptySectionException ignored){} + catch (Exception e){ + this.ncaContent0 = null; + e.printStackTrace(); + } + try{ + this.ncaContent1 = new NCAContent(file, offset, sectionBlock1, tableEntry1, key); + } + catch (EmptySectionException ignored){} + catch (Exception e){ + this.ncaContent1 = null; + e.printStackTrace(); + } + try{ + this.ncaContent2 = new NCAContent(file, offset, sectionBlock2, tableEntry2, key); + } + catch (EmptySectionException ignored){} + catch (Exception e){ + this.ncaContent2 = null; + e.printStackTrace(); + } + try{ + this.ncaContent3 = new NCAContent(file, offset, sectionBlock3, tableEntry3, key); + } + catch (EmptySectionException ignored){} + catch (Exception e){ + this.ncaContent3 = null; + e.printStackTrace(); + } + } + + public byte[] getRsa2048one() { return rsa2048one; } + public byte[] getRsa2048two() { return rsa2048two; } + public String getMagicnum() { return magicnum; } + public byte getSystemOrGcIndicator() { return systemOrGcIndicator; } + public byte getContentType() { return contentType; } + public byte getCryptoType1() { return cryptoType1; } + public byte getKeyIndex() { return keyIndex; } + public long getNcaSize() { return ncaSize; } + public byte[] getTitleId() { return titleId; } + public byte[] getContentIndx() { return contentIndx; } + public byte[] getSdkVersion() { return sdkVersion; } + public byte getCryptoType2() { return cryptoType2; } + public byte getHeader1SignatureKeyGeneration() { return Header1SignatureKeyGeneration; } + public byte[] getKeyGenerationReserved() { return keyGenerationReserved; } + public byte[] getRightsId() { return rightsId; } + + public byte[] getSha256hash0() { return sha256hash0; } + public byte[] getSha256hash1() { return sha256hash1; } + public byte[] getSha256hash2() { return sha256hash2; } + public byte[] getSha256hash3() { return sha256hash3; } + + public byte[] getEncryptedKey0() { return encryptedKey0; } + public byte[] getEncryptedKey1() { return encryptedKey1; } + public byte[] getEncryptedKey2() { return encryptedKey2; } + public byte[] getEncryptedKey3() { return encryptedKey3; } + + public byte[] getDecryptedKey0() { return decryptedKey0; } + public byte[] getDecryptedKey1() { return decryptedKey1; } + public byte[] getDecryptedKey2() { return decryptedKey2; } + public byte[] getDecryptedKey3() { return decryptedKey3; } + + public NCAHeaderTableEntry getTableEntry0() { return tableEntry0; } + public NCAHeaderTableEntry getTableEntry1() { return tableEntry1; } + public NCAHeaderTableEntry getTableEntry2() { return tableEntry2; } + public NCAHeaderTableEntry getTableEntry3() { return tableEntry3; } + + public NCASectionBlock getSectionBlock0() { return sectionBlock0; } + public NCASectionBlock getSectionBlock1() { return sectionBlock1; } + public NCASectionBlock getSectionBlock2() { return sectionBlock2; } + public NCASectionBlock getSectionBlock3() { return sectionBlock3; } + + public boolean isKeyAvailable(){ // TODO: USE + if (Arrays.equals(rightsId, new byte[0x10])) + return true; + else + return keys.containsKey(byteArrToHexString(rightsId)); + } + /** + * Get content for the selected section + * @param sectionNumber should be 0-3 + * */ + public NCAContent getNCAContentProvider(int sectionNumber){ + switch (sectionNumber) { + case 0: + return ncaContent0; + case 1: + return ncaContent1; + case 2: + return ncaContent2; + case 3: + return ncaContent3; + default: + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/libKonogonka/Tools/NCA/NCASectionTableBlock/NCASectionBlock.java b/src/main/java/libKonogonka/Tools/NCA/NCASectionTableBlock/NCASectionBlock.java new file mode 100644 index 0000000..55dfff0 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NCA/NCASectionTableBlock/NCASectionBlock.java @@ -0,0 +1,113 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NCA.NCASectionTableBlock; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.getLEint; +import static libKonogonka.LoperConverter.getLElong; + +public class NCASectionBlock { + private byte[] version; + private byte fsType; + private byte hashType; + private byte cryptoType; + private byte[] padding; + private SuperBlockIVFC superBlockIVFC; + private SuperBlockPFS0 superBlockPFS0; + private byte[] BKTRfullHeader; + // BKTR extended + private long BKTRoffsetSection1; + private long BKTRsizeSection1; + private String BKTRmagicSection1; + private int BKTRu32Section1; + private int BKTRs32Section1; + private byte[] BKTRunknownSection1; + + private long BKTRoffsetSection2; + private long BKTRsizeSection2; + private String BKTRmagicSection2; + private int BKTRu32Section2; + private int BKTRs32Section2; + private byte[] BKTRunknownSection2; + + private byte[] sectionCTR; + private byte[] unknownEndPadding; + + public NCASectionBlock(byte[] tableBlockBytes) throws Exception{ + if (tableBlockBytes.length != 0x200) + throw new Exception("Table Block Section size is incorrect."); + version = Arrays.copyOfRange(tableBlockBytes, 0, 0x2); + fsType = tableBlockBytes[0x2]; + hashType = tableBlockBytes[0x3]; + cryptoType = tableBlockBytes[0x4]; + padding = Arrays.copyOfRange(tableBlockBytes, 0x5, 0x8); + byte[] superBlockBytes = Arrays.copyOfRange(tableBlockBytes, 0x8, 0xf8); + + if ((fsType == 0) && (hashType == 0x3)) + superBlockIVFC = new SuperBlockIVFC(superBlockBytes); + else if ((fsType == 0x1) && (hashType == 0x2)) + superBlockPFS0 = new SuperBlockPFS0(superBlockBytes); + + BKTRfullHeader = Arrays.copyOfRange(tableBlockBytes, 0x100, 0x140); + + BKTRoffsetSection1 = getLElong(BKTRfullHeader, 0); + BKTRsizeSection1 = getLElong(BKTRfullHeader, 0x8); + BKTRmagicSection1 = new String(Arrays.copyOfRange(BKTRfullHeader, 0x10, 0x14), StandardCharsets.US_ASCII); + BKTRu32Section1 = getLEint(BKTRfullHeader, 0x14); + BKTRs32Section1 = getLEint(BKTRfullHeader, 0x18); + BKTRunknownSection1 = Arrays.copyOfRange(tableBlockBytes, 0x1c, 0x20); + + BKTRoffsetSection2 = getLElong(BKTRfullHeader, 0x20); + BKTRsizeSection2 = getLElong(BKTRfullHeader, 0x28); + BKTRmagicSection2 = new String(Arrays.copyOfRange(BKTRfullHeader, 0x30, 0x34), StandardCharsets.US_ASCII); + BKTRu32Section2 = getLEint(BKTRfullHeader, 0x34); + BKTRs32Section2 = getLEint(BKTRfullHeader, 0x38); + BKTRunknownSection2 = Arrays.copyOfRange(BKTRfullHeader, 0x3c, 0x40); + + sectionCTR = Arrays.copyOfRange(tableBlockBytes, 0x140, 0x148); + unknownEndPadding = Arrays.copyOfRange(tableBlockBytes, 0x148, 0x200); + } + + public byte[] getVersion() { return version; } + public byte getFsType() { return fsType; } + public byte getHashType() { return hashType; } + public byte getCryptoType() { return cryptoType; } + public byte[] getPadding() { return padding; } + public SuperBlockIVFC getSuperBlockIVFC() { return superBlockIVFC; } + public SuperBlockPFS0 getSuperBlockPFS0() { return superBlockPFS0; } + public byte[] getBKTRfullHeader() { return BKTRfullHeader; } + + public long getBKTRoffsetSection1() { return BKTRoffsetSection1; } + public long getBKTRsizeSection1() { return BKTRsizeSection1; } + public String getBKTRmagicSection1() { return BKTRmagicSection1; } + public int getBKTRu32Section1() { return BKTRu32Section1; } + public int getBKTRs32Section1() { return BKTRs32Section1; } + public byte[] getBKTRunknownSection1() { return BKTRunknownSection1; } + public long getBKTRoffsetSection2() { return BKTRoffsetSection2; } + public long getBKTRsizeSection2() { return BKTRsizeSection2; } + public String getBKTRmagicSection2() { return BKTRmagicSection2; } + public int getBKTRu32Section2() { return BKTRu32Section2; } + public int getBKTRs32Section2() { return BKTRs32Section2; } + public byte[] getBKTRunknownSection2() { return BKTRunknownSection2; } + public byte[] getSectionCTR() { return sectionCTR; } + public byte[] getUnknownEndPadding() { return unknownEndPadding; } +} + diff --git a/src/main/java/libKonogonka/Tools/NCA/NCASectionTableBlock/SuperBlockIVFC.java b/src/main/java/libKonogonka/Tools/NCA/NCASectionTableBlock/SuperBlockIVFC.java new file mode 100644 index 0000000..a2aaec4 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NCA/NCASectionTableBlock/SuperBlockIVFC.java @@ -0,0 +1,173 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NCA.NCASectionTableBlock; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.getLEint; +import static libKonogonka.LoperConverter.getLElong; + +public class SuperBlockIVFC { + private String magic; + private int magicNumber; + private int masterHashSize; + private int totalNumberOfLevels; + private long lvl1Offset; + private long lvl1Size; + private int lvl1SBlockSize; + private byte[] reserved1; + + private long lvl2Offset; + private long lvl2Size; + private int lvl2SBlockSize; + private byte[] reserved2; + + private long lvl3Offset; + private long lvl3Size; + private int lvl3SBlockSize; + private byte[] reserved3; + + private long lvl4Offset; + private long lvl4Size; + private int lvl4SBlockSize; + private byte[] reserved4; + + private long lvl5Offset; + private long lvl5Size; + private int lvl5SBlockSize; + private byte[] reserved5; + + private long lvl6Offset; + private long lvl6Size; + private int lvl6SBlockSize; + private byte[] reserved6; + + private byte[] unknown; + private byte[] hash; + + SuperBlockIVFC(byte[] sbBytes){ + this.magic = new String(Arrays.copyOfRange(sbBytes, 0, 4), StandardCharsets.US_ASCII); + this.magicNumber = getLEint(sbBytes, 0x4); + this.masterHashSize = getLEint(sbBytes, 0x8); + this.totalNumberOfLevels = getLEint(sbBytes, 0xc); + + this.lvl1Offset = getLElong(sbBytes, 0x10); + this.lvl1Size = getLElong(sbBytes, 0x18); + this.lvl1SBlockSize = getLEint(sbBytes, 0x20); + this.reserved1 = Arrays.copyOfRange(sbBytes, 0x24, 0x28); + + this.lvl2Offset = getLElong(sbBytes, 0x28); + this.lvl2Size = getLElong(sbBytes, 0x30); + this.lvl2SBlockSize = getLEint(sbBytes, 0x38); + this.reserved2 = Arrays.copyOfRange(sbBytes, 0x3c, 0x40); + + this.lvl3Offset = getLElong(sbBytes, 0x40); + this.lvl3Size = getLElong(sbBytes, 0x48); + this.lvl3SBlockSize = getLEint(sbBytes, 0x50); + this.reserved3 = Arrays.copyOfRange(sbBytes, 0x54, 0x58); + + this.lvl4Offset = getLElong(sbBytes, 0x58); + this.lvl4Size = getLElong(sbBytes, 0x60); + this.lvl4SBlockSize = getLEint(sbBytes, 0x68); + this.reserved4 = Arrays.copyOfRange(sbBytes, 0x6c, 0x70); + + this.lvl5Offset = getLElong(sbBytes, 0x70); + this.lvl5Size = getLElong(sbBytes, 0x78); + this.lvl5SBlockSize = getLEint(sbBytes, 0x80); + this.reserved5 = Arrays.copyOfRange(sbBytes, 0x84, 0x88); + + this.lvl6Offset = getLElong(sbBytes, 0x88); + this.lvl6Size = getLElong(sbBytes, 0x90); + this.lvl6SBlockSize = getLEint(sbBytes, 0x98); + this.reserved6 = Arrays.copyOfRange(sbBytes, 0x9c, 0xa0); + + this.unknown = Arrays.copyOfRange(sbBytes, 0xa0, 0xc0); + this.hash = Arrays.copyOfRange(sbBytes, 0xc0, 0xe0); + /* + System.out.println(magic); + System.out.println(magicNumber); + System.out.println(masterHashSize); + System.out.println(totalNumberOfLevels); + System.out.println(lvl1Offset); + System.out.println(lvl1Size); + System.out.println(lvl1SBlockSize); + RainbowHexDump.hexDumpUTF8(reserved1); + + System.out.println(lvl2Offset); + System.out.println(lvl2Size); + System.out.println(lvl2SBlockSize); + RainbowHexDump.hexDumpUTF8(reserved2); + + System.out.println(lvl3Offset); + System.out.println(lvl3Size); + System.out.println(lvl3SBlockSize); + RainbowHexDump.hexDumpUTF8(reserved3); + + System.out.println(lvl4Offset); + System.out.println(lvl4Size); + System.out.println(lvl4SBlockSize); + RainbowHexDump.hexDumpUTF8(reserved4); + + System.out.println(lvl5Offset); + System.out.println(lvl5Size); + System.out.println(lvl5SBlockSize); + RainbowHexDump.hexDumpUTF8(reserved5); + + System.out.println(lvl6Offset); + System.out.println(lvl6Size); + System.out.println(lvl6SBlockSize); + RainbowHexDump.hexDumpUTF8(reserved6); + + RainbowHexDump.hexDumpUTF8(unknown); + RainbowHexDump.hexDumpUTF8(hash); + // */ + } + + public String getMagic() { return magic; } + public int getMagicNumber() { return magicNumber; } + public int getMasterHashSize() { return masterHashSize; } + public int getTotalNumberOfLevels() { return totalNumberOfLevels; } + public long getLvl1Offset() { return lvl1Offset; } + public long getLvl1Size() { return lvl1Size; } + public int getLvl1SBlockSize() { return lvl1SBlockSize; } + public byte[] getReserved1() { return reserved1; } + public long getLvl2Offset() { return lvl2Offset; } + public long getLvl2Size() { return lvl2Size; } + public int getLvl2SBlockSize() { return lvl2SBlockSize; } + public byte[] getReserved2() { return reserved2; } + public long getLvl3Offset() { return lvl3Offset; } + public long getLvl3Size() { return lvl3Size; } + public int getLvl3SBlockSize() { return lvl3SBlockSize; } + public byte[] getReserved3() { return reserved3; } + public long getLvl4Offset() { return lvl4Offset; } + public long getLvl4Size() { return lvl4Size; } + public int getLvl4SBlockSize() { return lvl4SBlockSize; } + public byte[] getReserved4() { return reserved4; } + public long getLvl5Offset() { return lvl5Offset; } + public long getLvl5Size() { return lvl5Size; } + public int getLvl5SBlockSize() { return lvl5SBlockSize; } + public byte[] getReserved5() { return reserved5; } + public long getLvl6Offset() { return lvl6Offset; } + public long getLvl6Size() { return lvl6Size; } + public int getLvl6SBlockSize() { return lvl6SBlockSize; } + public byte[] getReserved6() { return reserved6; } + public byte[] getUnknown() { return unknown; } + public byte[] getHash() { return hash; } +} diff --git a/src/main/java/libKonogonka/Tools/NCA/NCASectionTableBlock/SuperBlockPFS0.java b/src/main/java/libKonogonka/Tools/NCA/NCASectionTableBlock/SuperBlockPFS0.java new file mode 100644 index 0000000..c755eea --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NCA/NCASectionTableBlock/SuperBlockPFS0.java @@ -0,0 +1,55 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NCA.NCASectionTableBlock; + +import java.util.Arrays; + +import static libKonogonka.LoperConverter.getLEint; +import static libKonogonka.LoperConverter.getLElong; + +public class SuperBlockPFS0 { + private byte[] SHA256hash; + private int blockSize; + private int unknownNumberTwo; + private long hashTableOffset; + private long hashTableSize; + private long pfs0offset; + private long pfs0size; + private byte[] zeroes; + + SuperBlockPFS0(byte[] sbBytes){ + SHA256hash = Arrays.copyOfRange(sbBytes, 0, 0x20); + blockSize = getLEint(sbBytes, 0x20); + unknownNumberTwo = getLEint(sbBytes, 0x24); + hashTableOffset = getLElong(sbBytes, 0x28); + hashTableSize = getLElong(sbBytes, 0x30); + pfs0offset = getLElong(sbBytes, 0x38); + pfs0size = getLElong(sbBytes, 0x40); + zeroes = Arrays.copyOfRange(sbBytes, 0x48, 0xf8); + } + + public byte[] getSHA256hash() { return SHA256hash; } + public int getBlockSize() { return blockSize; } + public int getUnknownNumberTwo() { return unknownNumberTwo; } + public long getHashTableOffset() { return hashTableOffset; } + public long getHashTableSize() { return hashTableSize; } + public long getPfs0offset() { return pfs0offset; } + public long getPfs0size() { return pfs0size; } + public byte[] getZeroes() { return zeroes; } +} diff --git a/src/main/java/libKonogonka/Tools/NPDM/ACI0/ACI0Provider.java b/src/main/java/libKonogonka/Tools/NPDM/ACI0/ACI0Provider.java new file mode 100644 index 0000000..cdb55cf --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NPDM/ACI0/ACI0Provider.java @@ -0,0 +1,81 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NPDM.ACI0; + +import libKonogonka.Tools.NPDM.KernelAccessControlProvider; +import libKonogonka.Tools.NPDM.ServiceAccessControlProvider; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.getLEint; + +public class ACI0Provider { + private String magicNum; + private byte[] reserved1; + private byte[] titleID; + private byte[] reserved2; + private int fsAccessHeaderOffset; + private int fsAccessHeaderSize; + private int serviceAccessControlOffset; + private int serviceAccessControlSize; + private int kernelAccessControlOffset; + private int kernelAccessControlSize; + private byte[] reserved3; + + private FSAccessHeaderProvider fsAccessHeaderProvider; + private ServiceAccessControlProvider serviceAccessControlProvider; + private KernelAccessControlProvider kernelAccessControlProvider; + + public ACI0Provider(byte[] aci0bytes) throws Exception { + if (aci0bytes.length < 0x40) + throw new Exception("ACI0 size is too short"); + magicNum = new String(aci0bytes, 0, 0x4, StandardCharsets.UTF_8); + reserved1 = Arrays.copyOfRange(aci0bytes, 0x4, 0x10); + titleID = Arrays.copyOfRange(aci0bytes, 0x10, 0x18); + reserved2 = Arrays.copyOfRange(aci0bytes, 0x18, 0x20); + fsAccessHeaderOffset = getLEint(aci0bytes, 0x20); + fsAccessHeaderSize = getLEint(aci0bytes, 0x24); + serviceAccessControlOffset = getLEint(aci0bytes, 0x28); + serviceAccessControlSize = getLEint(aci0bytes, 0x2C); + kernelAccessControlOffset = getLEint(aci0bytes, 0x30); + kernelAccessControlSize = getLEint(aci0bytes, 0x34); + reserved3 = Arrays.copyOfRange(aci0bytes, 0x38, 0x40); + + fsAccessHeaderProvider = new FSAccessHeaderProvider(Arrays.copyOfRange(aci0bytes, fsAccessHeaderOffset, fsAccessHeaderOffset+fsAccessHeaderSize)); + serviceAccessControlProvider = new ServiceAccessControlProvider(Arrays.copyOfRange(aci0bytes, serviceAccessControlOffset, serviceAccessControlOffset+serviceAccessControlSize)); + kernelAccessControlProvider = new KernelAccessControlProvider(Arrays.copyOfRange(aci0bytes, kernelAccessControlOffset, kernelAccessControlOffset+kernelAccessControlSize)); + } + + public String getMagicNum() { return magicNum; } + public byte[] getReserved1() { return reserved1; } + public byte[] getTitleID() { return titleID; } + public byte[] getReserved2() { return reserved2; } + public int getFsAccessHeaderOffset() { return fsAccessHeaderOffset; } + public int getFsAccessHeaderSize() { return fsAccessHeaderSize; } + public int getServiceAccessControlOffset() { return serviceAccessControlOffset; } + public int getServiceAccessControlSize() { return serviceAccessControlSize; } + public int getKernelAccessControlOffset() { return kernelAccessControlOffset; } + public int getKernelAccessControlSize() { return kernelAccessControlSize; } + public byte[] getReserved3() { return reserved3; } + + public FSAccessHeaderProvider getFsAccessHeaderProvider() { return fsAccessHeaderProvider; } + public ServiceAccessControlProvider getServiceAccessControlProvider() { return serviceAccessControlProvider; } + public KernelAccessControlProvider getKernelAccessControlProvider() { return kernelAccessControlProvider; } +} \ No newline at end of file diff --git a/src/main/java/libKonogonka/Tools/NPDM/ACI0/FSAccessHeaderProvider.java b/src/main/java/libKonogonka/Tools/NPDM/ACI0/FSAccessHeaderProvider.java new file mode 100644 index 0000000..2cbe5d1 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NPDM/ACI0/FSAccessHeaderProvider.java @@ -0,0 +1,58 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NPDM.ACI0; + +import libKonogonka.LoperConverter; + +import java.util.Arrays; + +/** + * For ACI0 Provider + * */ +public class FSAccessHeaderProvider { + + private byte version; + private byte[] padding; + private long permissionsBitmask; + private int dataSize; + private int contentOwnIdSectionSize; + private int dataNownerSizes; + private int saveDataOwnSectionSize; + private byte[] unknownData; + + public FSAccessHeaderProvider(byte[] bytes) { + version = bytes[0]; + padding = Arrays.copyOfRange(bytes, 1, 0x4); + permissionsBitmask = LoperConverter.getLElong(bytes, 0x4); + dataSize = LoperConverter.getLEint(bytes, 0xC); + contentOwnIdSectionSize = LoperConverter.getLEint(bytes, 0x10); + dataNownerSizes = LoperConverter.getLEint(bytes, 0x14); + saveDataOwnSectionSize = LoperConverter.getLEint(bytes, 0x18); + unknownData = Arrays.copyOfRange(bytes, 0x1C, bytes.length); + } + + public byte getVersion() { return version; } + public byte[] getPadding() { return padding; } + public long getPermissionsBitmask() { return permissionsBitmask; } + public int getDataSize() { return dataSize; } + public int getContentOwnIdSectionSize() { return contentOwnIdSectionSize; } + public int getDataNownerSizes() { return dataNownerSizes; } + public int getSaveDataOwnSectionSize() { return saveDataOwnSectionSize; } + public byte[] getUnknownData() { return unknownData; } +} diff --git a/src/main/java/libKonogonka/Tools/NPDM/ACID/ACIDProvider.java b/src/main/java/libKonogonka/Tools/NPDM/ACID/ACIDProvider.java new file mode 100644 index 0000000..9734923 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NPDM/ACID/ACIDProvider.java @@ -0,0 +1,104 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NPDM.ACID; + +import libKonogonka.Tools.NPDM.KernelAccessControlProvider; +import libKonogonka.Tools.NPDM.ServiceAccessControlProvider; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.*; + +public class ACIDProvider { + + private byte[] rsa2048signature; + private byte[] rsa2048publicKey; + private String magicNum; + private int dataSize; + private byte[] reserved1; + private byte flag1; + private byte flag2; + private byte flag3; + private byte flag4; + private long titleRangeMin; + private long titleRangeMax; + private int fsAccessControlOffset; + private int fsAccessControlSize; + private int serviceAccessControlOffset; + private int serviceAccessControlSize; + private int kernelAccessControlOffset; + private int kernelAccessControlSize; + private byte[] reserved2; + + private FSAccessControlProvider fsAccessControlProvider; + private ServiceAccessControlProvider serviceAccessControlProvider; + private KernelAccessControlProvider kernelAccessControlProvider; + + public ACIDProvider(byte[] acidBytes) throws Exception{ + if (acidBytes.length < 0x240) + throw new Exception("ACIDProvider -> ACI0 size is too short"); + rsa2048signature = Arrays.copyOfRange(acidBytes, 0, 0x100); + rsa2048publicKey = Arrays.copyOfRange(acidBytes, 0x100, 0x200); + magicNum = new String(acidBytes, 0x200, 0x4, StandardCharsets.UTF_8); + dataSize = getLEint(acidBytes, 0x204); + reserved1 = Arrays.copyOfRange(acidBytes, 0x208, 0x20C); + flag1 = acidBytes[0x20C]; + flag2 = acidBytes[0x20D]; + flag3 = acidBytes[0x20E]; + flag4 = acidBytes[0x20F]; + titleRangeMin = getLElong(acidBytes, 0x210); + titleRangeMax = getLElong(acidBytes, 0x218); + fsAccessControlOffset = getLEint(acidBytes, 0x220); + fsAccessControlSize = getLEint(acidBytes, 0x224); + serviceAccessControlOffset = getLEint(acidBytes, 0x228); + serviceAccessControlSize = getLEint(acidBytes, 0x22C); + kernelAccessControlOffset = getLEint(acidBytes, 0x230); + kernelAccessControlSize = getLEint(acidBytes, 0x234); + reserved2 = Arrays.copyOfRange(acidBytes, 0x238, 0x240); + if (fsAccessControlOffset > serviceAccessControlOffset || serviceAccessControlOffset > kernelAccessControlOffset ) + throw new Exception("ACIDProvider -> blocks inside the ACID are not sorted in ascending order. Only ascending order supported."); + fsAccessControlProvider = new FSAccessControlProvider(Arrays.copyOfRange(acidBytes, fsAccessControlOffset, fsAccessControlOffset+fsAccessControlSize)); + serviceAccessControlProvider = new ServiceAccessControlProvider(Arrays.copyOfRange(acidBytes, serviceAccessControlOffset, serviceAccessControlOffset+serviceAccessControlSize)); + kernelAccessControlProvider = new KernelAccessControlProvider(Arrays.copyOfRange(acidBytes, kernelAccessControlOffset, kernelAccessControlOffset+kernelAccessControlSize)); + } + + public byte[] getRsa2048signature() { return rsa2048signature; } + public byte[] getRsa2048publicKey() { return rsa2048publicKey; } + public String getMagicNum() { return magicNum; } + public int getDataSize() { return dataSize; } + public byte[] getReserved1() { return reserved1; } + public byte getFlag1() { return flag1; } + public byte getFlag2() { return flag2; } + public byte getFlag3() { return flag3; } + public byte getFlag4() { return flag4; } + public long getTitleRangeMin() { return titleRangeMin; } + public long getTitleRangeMax() { return titleRangeMax; } + public int getFsAccessControlOffset() { return fsAccessControlOffset; } + public int getFsAccessControlSize() { return fsAccessControlSize; } + public int getServiceAccessControlOffset() { return serviceAccessControlOffset; } + public int getServiceAccessControlSize() { return serviceAccessControlSize; } + public int getKernelAccessControlOffset() { return kernelAccessControlOffset; } + public int getKernelAccessControlSize() { return kernelAccessControlSize; } + public byte[] getReserved2() { return reserved2; } + + public FSAccessControlProvider getFsAccessControlProvider() { return fsAccessControlProvider; } + public ServiceAccessControlProvider getServiceAccessControlProvider() { return serviceAccessControlProvider; } + public KernelAccessControlProvider getKernelAccessControlProvider() { return kernelAccessControlProvider; } +} \ No newline at end of file diff --git a/src/main/java/libKonogonka/Tools/NPDM/ACID/FSAccessControlProvider.java b/src/main/java/libKonogonka/Tools/NPDM/ACID/FSAccessControlProvider.java new file mode 100644 index 0000000..4fc89a2 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NPDM/ACID/FSAccessControlProvider.java @@ -0,0 +1,46 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NPDM.ACID; + +import libKonogonka.LoperConverter; + +import java.util.Arrays; + +/** + * For ACID Provider + * */ +public class FSAccessControlProvider { + + private byte version; + private byte[] padding; + private long permissionsBitmask; + private byte[] reserved; + + public FSAccessControlProvider(byte[] bytes) { + version = bytes[0]; + padding = Arrays.copyOfRange(bytes, 1, 0x4); + permissionsBitmask = LoperConverter.getLElong(bytes, 0x4); + reserved = Arrays.copyOfRange(bytes, 0xC, 0x2C); + } + + public byte getVersion() { return version; } + public byte[] getPadding() { return padding; } + public long getPermissionsBitmask() { return permissionsBitmask; } + public byte[] getReserved() { return reserved; } +} diff --git a/src/main/java/libKonogonka/Tools/NPDM/KernelAccessControlProvider.java b/src/main/java/libKonogonka/Tools/NPDM/KernelAccessControlProvider.java new file mode 100644 index 0000000..48e813b --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NPDM/KernelAccessControlProvider.java @@ -0,0 +1,246 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NPDM; + +import libKonogonka.LoperConverter; + +import java.util.LinkedHashMap; +import java.util.LinkedList; + +/* +NOTE: This implementation is extremely bad for using application as library. Use raw for own purposes. + +NOTE: +KAC is set of 4-byes blocks +Consider them as uInt32 (Read as Little endian) +Look on the tail of each block (low bits). If tail is equals to mask like 0111111 then such block is related to one of the possible sections (KernelFlags etc.) +If it's related to the one of the blocks, then we could pick useful data from this block. +Example: +36 BYES on this section, then 9 blocks with len = 4-bytes each available +1 00-01-02-03 +2 04-05-06-07 +3 08-09-10-11 +4 12-13-14-15 +5 16-17-18-19 +6 20-21-22-23 +7 24-25-26-27 +8 28-29-30-31 +9 32-33-34-35 + +Possible patterns are: +Where '+' is useful data; '0' and '1' in low bytes are pattern. +Octal | Decimal +++++++++++++++++++++++++++++0111 | 7 <- KernelFlags ++++++++++++++++++++++++++++01111 | 15 <- SyscallMask ++++++++++++++++++++++++++0111111 | 63 <- MapIoOrNormalRange +++++++++++++++++++++++++01111111 | 127 <- MapNormalPage (RW) +++++++++++++++++++++011111111111 | 2+47 <- InterruptPair +++++++++++++++++++01111111111111 | 8191 <- ApplicationType ++++++++++++++++++011111111111111 | 16383 <- KernelReleaseVersion +++++++++++++++++0111111111111111 | 32767 <- HandleTableSize ++++++++++++++++01111111111111111 | 65535 <- DebugFlags +Other masks could be implemented by N in future (?). + +Calculation example: +Dec 1 = 00000000000000000000000000000001 +00100000000000000000000000000111 & 1 = 1 +00010000000000000000000000000011 & 1 = 1 +00001000000000000000000000000001 & 1 = 1 +00000100000000000000000000000000 & 1 = 0 + +TIP: Generate +int j = 0xFFFFFFFF; +for (byte i = 0; i < 16; i++){ + j = (j << 1); + RainbowHexDump.octDumpInt(~j); +} + */ + +public class KernelAccessControlProvider { + + private static final int KERNELFLAGS = 3, + SYSCALLMASK = 4, + MAPIOORNORMALRANGE = 6, + MAPNORMALPAGE_RW = 7, + INTERRUPTPAIR = 11, + APPLICATIONTYPE = 13, + KERNELRELEASEVERSION = 14, + HANDLETABLESIZE = 15, + DEBUGFLAGS = 16; + + // RAW data + private LinkedList rawData; + // Kernel flags + private boolean kernelFlagsAvailable; + private int kernelFlagCpuIdHi, + kernelFlagCpuIdLo, + kernelFlagThreadPrioHi, + kernelFlagThreadPrioLo; + // Syscall Masks as index | mask - order AS IS. [0] = bit5; [1] = bit6 + private LinkedHashMap syscallMasks; // Index, Mask + // MapIoOrNormalRange + private LinkedHashMap mapIoOrNormalRange; // alt page+num, RO flag + // MapNormalPage (RW) + private byte[] mapNormalPage; // TODO: clarify is possible to have multiple + // InterruptPair + private LinkedHashMap interruptPairs; // Number; irq0, irq2 + // Application type + private int applicationType; + // KernelReleaseVersion + private boolean isKernelRelVersionAvailable; + private int kernelRelVersionMajor, + kernelRelVersionMinor; + // Handle Table Size + private int handleTableSize; + // Debug flags + private boolean debugFlagsAvailable, + canBeDebugged, + canDebugOthers; + + public KernelAccessControlProvider(byte[] bytes) throws Exception{ + if (bytes.length < 4) + throw new Exception("ACID-> KernelAccessControlProvider: too small size of the Kernel Access Control"); + + rawData = new LinkedList(); + + interruptPairs = new LinkedHashMap<>(); + syscallMasks = new LinkedHashMap(); + mapIoOrNormalRange = new LinkedHashMap(); + + int position = 0; + // Collect all blocks + for (int i = 0; i < bytes.length / 4; i++) { + int block = LoperConverter.getLEint(bytes, position); + position += 4; + + rawData.add(block); + + //RainbowHexDump.octDumpInt(block); + + int type = getMinBitCnt(block); + + switch (type){ + case KERNELFLAGS: + kernelFlagsAvailable = true; + kernelFlagCpuIdHi = block >> 24; + kernelFlagCpuIdLo = block >> 16 & 0b11111111; + kernelFlagThreadPrioHi = block >> 10 & 0b111111; + kernelFlagThreadPrioLo = block >> 4 & 0b111111; + //System.out.println("KERNELFLAGS "+kernelFlagCpuIdHi+" "+kernelFlagCpuIdLo+" "+kernelFlagThreadPrioHi+" "+kernelFlagThreadPrioLo); + break; + case SYSCALLMASK: + byte maskTableIndex = (byte) (block >> 29 & 0b111); // declared as byte; max value could be 7; min - 0; + byte[] mask = new byte[24]; // Consider as bit. + //System.out.println("SYSCALLMASK ind: "+maskTableIndex); + + for (int k = 28; k >= 5; k--) { + mask[k-5] = (byte) (block >> k & 1); // Only 1 or 0 possible + //System.out.print(mask[k-5]); + } + //System.out.println(); + syscallMasks.put(maskTableIndex, mask); + break; + case MAPIOORNORMALRANGE: + byte[] altStPgNPgNum = new byte[24]; + //System.out.println("MAPIOORNORMALRANGE Flag: "+((block >> 31 & 1) != 0)); + + for (int k = 30; k >= 7; k--){ + altStPgNPgNum[k-7] = (byte) (block >> k & 1); // Only 1 or 0 possible + //System.out.print(altStPgNPgNum[k-7]); + } + mapIoOrNormalRange.put(altStPgNPgNum, (block >> 31 & 1) != 0); + //System.out.println(); + break; + case MAPNORMALPAGE_RW: + //System.out.println("MAPNORMALPAGE_RW\t"); + mapNormalPage = new byte[24]; + for (int k = 31; k >= 8; k--){ + mapNormalPage[k-8] = (byte) (block >> k & 1); + //System.out.print(mapNormalPage[k-8]); + } + //System.out.println(); + break; + case INTERRUPTPAIR: + //System.out.println("INTERRUPTPAIR"); + //RainbowHexDump.octDumpInt(block); + byte[][] pair = new byte[2][]; + byte[] irq0 = new byte[10]; + byte[] irq1 = new byte[10]; + + for (int k = 21; k >= 12; k--) + irq0[k-12] = (byte) (block >> k & 1); + for (int k = 31; k >= 22; k--) + irq1[k-22] = (byte) (block >> k & 1); + pair[0] = irq0; + pair[1] = irq1; + interruptPairs.put(interruptPairs.size(), pair); + break; + case APPLICATIONTYPE: + applicationType = block >> 14 & 0b111; + //System.out.println("APPLICATIONTYPE "+applicationType); + break; + case KERNELRELEASEVERSION: + //System.out.println("KERNELRELEASEVERSION\t"+(block >> 19 & 0b111111111111)+"."+(block >> 15 & 0b1111)+".X"); + isKernelRelVersionAvailable = true; + kernelRelVersionMajor = (block >> 19 & 0b111111111111); + kernelRelVersionMinor = (block >> 15 & 0b1111); + break; + case HANDLETABLESIZE: + handleTableSize = block >> 16 & 0b1111111111; + //System.out.println("HANDLETABLESIZE "+handleTableSize); + break; + case DEBUGFLAGS: + debugFlagsAvailable = true; + canBeDebugged = (block >> 17 & 1) != 0; + canDebugOthers = (block >> 18 & 1) != 0; + //System.out.println("DEBUGFLAGS "+canBeDebugged+" "+canDebugOthers); + break; + default: + System.out.println("UNKNOWN\t\t"+block+" "+type); + } + } + } + + private int getMinBitCnt(int value){ + int minBitCnt = 0; + while ((value & 1) != 0){ + value >>= 1; + minBitCnt++; + } + return minBitCnt; + } + public LinkedList getRawData() { return rawData; } + public boolean isKernelFlagsAvailable() { return kernelFlagsAvailable; } + public int getKernelFlagCpuIdHi() { return kernelFlagCpuIdHi; } + public int getKernelFlagCpuIdLo() { return kernelFlagCpuIdLo; } + public int getKernelFlagThreadPrioHi() { return kernelFlagThreadPrioHi; } + public int getKernelFlagThreadPrioLo() { return kernelFlagThreadPrioLo; } + public LinkedHashMap getMapIoOrNormalRange() { return mapIoOrNormalRange; } + public byte[] getMapNormalPage() { return mapNormalPage; } + public LinkedHashMap getInterruptPairs() { return interruptPairs; } + public int getApplicationType() { return applicationType; } + public boolean isKernelRelVersionAvailable() { return isKernelRelVersionAvailable; } + public int getKernelRelVersionMajor() { return kernelRelVersionMajor; } + public int getKernelRelVersionMinor() { return kernelRelVersionMinor;} + public int getHandleTableSize() { return handleTableSize; } + public boolean isDebugFlagsAvailable() { return debugFlagsAvailable; } + public boolean isCanBeDebugged() { return canBeDebugged; } + public boolean isCanDebugOthers() { return canDebugOthers; } + public LinkedHashMap getSyscallMasks() { return syscallMasks; } +} diff --git a/src/main/java/libKonogonka/Tools/NPDM/NPDMProvider.java b/src/main/java/libKonogonka/Tools/NPDM/NPDMProvider.java new file mode 100644 index 0000000..feac2e5 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NPDM/NPDMProvider.java @@ -0,0 +1,167 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NPDM; + +import libKonogonka.Tools.ASuperInFileProvider; +import libKonogonka.Tools.NPDM.ACI0.ACI0Provider; +import libKonogonka.Tools.NPDM.ACID.ACIDProvider; + +import java.io.File; +import java.io.PipedInputStream; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.*; + +public class NPDMProvider extends ASuperInFileProvider { + + private String magicNum; + private byte[] reserved1; + private byte MMUFlags; + private byte reserved2; + private byte mainThreadPrio; + private byte mainThreadCoreNum; + private byte[] reserved3; + private int personalMmHeapSize; // safe-to-store + private int version; // safe? + private long mainThreadStackSize; // TODO: check if safe + private String titleName; + private byte[] productCode; + private byte[] reserved4; + private int aci0offset; // originally 4-bytes (u-int) + private int aci0size; // originally 4-bytes (u-int) + private int acidOffset; // originally 4-bytes (u-int) + private int acidSize; // originally 4-bytes (u-int) + + private ACI0Provider aci0; + private ACIDProvider acid; + + public NPDMProvider(PipedInputStream pis) throws Exception{ + byte[] mainBuf = new byte[0x80]; + if(pis.read(mainBuf) != 0x80) + throw new Exception("NPDMProvider: Failed to read 'META'"); + aci0offset = getLEint(mainBuf, 0x70); + aci0size = getLEint(mainBuf, 0x74); + acidOffset = getLEint(mainBuf, 0x78); + acidSize = getLEint(mainBuf, 0x7C); + byte[] aci0Buf; + byte[] acidBuf; + if (aci0offset < acidOffset){ + if (pis.skip(aci0offset - 0x80) != (aci0offset - 0x80)) + throw new Exception("NPDMProvider: Failed to skip bytes till 'ACI0'"); + if ((aci0Buf = readFromStream(pis, aci0size)) == null) + throw new Exception("NPDMProvider: Failed to read 'ACI0'"); + if (pis.skip(acidOffset - aci0offset - aci0size) != (acidOffset - aci0offset - aci0size)) + throw new Exception("NPDMProvider: Failed to skip bytes till 'ACID'"); + if ((acidBuf = readFromStream(pis, acidSize)) == null) + throw new Exception("NPDMProvider: Failed to read 'ACID'"); + } + else { + if (pis.skip(acidOffset - 0x80) != (acidOffset - 0x80)) + throw new Exception("NPDMProvider: Failed to skip bytes till 'ACID'"); + if ((acidBuf = readFromStream(pis, acidSize)) == null) + throw new Exception("NPDMProvider: Failed to read 'ACID'"); + if (pis.skip(aci0offset - acidOffset - acidSize) != (aci0offset - acidOffset - acidSize)) + throw new Exception("NPDMProvider: Failed to skip bytes till 'ACI0'"); + if ((aci0Buf = readFromStream(pis, aci0size)) == null) + throw new Exception("NPDMProvider: Failed to read 'ACI0'"); + } + magicNum = new String(mainBuf, 0, 4, StandardCharsets.UTF_8); + reserved1 = Arrays.copyOfRange(mainBuf, 0x4, 0xC); + MMUFlags = mainBuf[0xC]; + reserved2 = mainBuf[0xD]; + mainThreadPrio = mainBuf[0xE]; + mainThreadCoreNum = mainBuf[0xF]; + reserved3 = Arrays.copyOfRange(mainBuf, 0x10, 0x14); + personalMmHeapSize = getLEint(mainBuf, 0x14); + version = getLEint(mainBuf, 0x18); + mainThreadStackSize = getLElongOfInt(mainBuf, 0x1C); + titleName = new String(mainBuf, 0x20, 0x10, StandardCharsets.UTF_8); + productCode = Arrays.copyOfRange(mainBuf, 0x30, 0x40); + reserved4 = Arrays.copyOfRange(mainBuf, 0x40, 0x70); + + aci0 = new ACI0Provider(aci0Buf); + acid = new ACIDProvider(acidBuf); + } + + public NPDMProvider(File file) throws Exception { this(file, 0); } + + public NPDMProvider(File file, long offset) throws Exception { + if (file.length() - offset < 0x80) // Header's size + throw new Exception("NPDMProvider: File is too small."); + RandomAccessFile raf = new RandomAccessFile(file, "r"); + raf.seek(offset); + // Get META + byte[] metaBuf = new byte[0x80]; + if (raf.read(metaBuf) != 0x80) + throw new Exception("NPDMProvider: Failed to read 'META'"); + magicNum = new String(metaBuf, 0, 4, StandardCharsets.UTF_8); + reserved1 = Arrays.copyOfRange(metaBuf, 0x4, 0xC); + MMUFlags = metaBuf[0xC]; + reserved2 = metaBuf[0xD]; + mainThreadPrio = metaBuf[0xE]; + mainThreadCoreNum = metaBuf[0xF]; + reserved3 = Arrays.copyOfRange(metaBuf, 0x10, 0x14); + personalMmHeapSize = getLEint(metaBuf, 0x14); + version = getLEint(metaBuf, 0x18); + mainThreadStackSize = getLElongOfInt(metaBuf, 0x1C); + titleName = new String(metaBuf, 0x20, 0x10, StandardCharsets.UTF_8); + productCode = Arrays.copyOfRange(metaBuf, 0x30, 0x40); + reserved4 = Arrays.copyOfRange(metaBuf, 0x40, 0x70); + aci0offset = getLEint(metaBuf, 0x70); + aci0size = getLEint(metaBuf, 0x74); + acidOffset = getLEint(metaBuf, 0x78); + acidSize = getLEint(metaBuf, 0x7C); + // Get ACI0 + raf.seek(aci0offset); + metaBuf = new byte[aci0size]; // TODO: NOTE: we read all size but it's memory consuming + if (raf.read(metaBuf) != aci0size) + throw new Exception("NPDMProvider: Failed to read 'ACI0'"); + aci0 = new ACI0Provider(metaBuf); + // Get ACID + raf.seek(acidOffset); + metaBuf = new byte[acidSize]; // TODO: NOTE: we read all size but it's memory consuming + if (raf.read(metaBuf) != acidSize) + throw new Exception("NPDMProvider: Failed to read 'ACID'"); + acid = new ACIDProvider(metaBuf); + raf.close(); + } + + public String getMagicNum() { return magicNum; } + public byte[] getReserved1() { return reserved1; } + public byte getMMUFlags() { return MMUFlags; } + public byte getReserved2() { return reserved2; } + public byte getMainThreadPrio() { return mainThreadPrio; } + public byte getMainThreadCoreNum() { return mainThreadCoreNum; } + public byte[] getReserved3() { return reserved3; } + public int getPersonalMmHeapSize() { return personalMmHeapSize; } + public int getVersion() { return version; } + public long getMainThreadStackSize() { return mainThreadStackSize; } + public String getTitleName() { return titleName; } + public byte[] getProductCode() { return productCode; } + public byte[] getReserved4() { return reserved4; } + public int getAci0offset() { return aci0offset; } + public int getAci0size() { return aci0size; } + public int getAcidOffset() { return acidOffset; } + public int getAcidSize() { return acidSize; } + + public ACI0Provider getAci0() { return aci0; } + public ACIDProvider getAcid() { return acid; } +} diff --git a/src/main/java/libKonogonka/Tools/NPDM/ServiceAccessControlProvider.java b/src/main/java/libKonogonka/Tools/NPDM/ServiceAccessControlProvider.java new file mode 100644 index 0000000..4394edd --- /dev/null +++ b/src/main/java/libKonogonka/Tools/NPDM/ServiceAccessControlProvider.java @@ -0,0 +1,48 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.NPDM; + +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; + +public class ServiceAccessControlProvider { + + private LinkedHashMap collection; + + public ServiceAccessControlProvider(byte[] bytes){ + collection = new LinkedHashMap<>(); + byte key; + String value; + + int i = 0; + + while (i < bytes.length){ + key = bytes[i]; + value = new String(bytes, i+1, getSize(key), StandardCharsets.UTF_8); + collection.put(value, key); + i += getSize(key)+1; + } + } + + private int getSize(byte control) { + return ((byte) 0x7 & control) + (byte) 0x01; + } + + public LinkedHashMap getCollection() { return collection; } +} diff --git a/src/main/java/libKonogonka/Tools/PFS0/IPFS0Provider.java b/src/main/java/libKonogonka/Tools/PFS0/IPFS0Provider.java new file mode 100644 index 0000000..5a19b01 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/PFS0/IPFS0Provider.java @@ -0,0 +1,31 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.PFS0; + +import libKonogonka.Tools.ISuperProvider; + +public interface IPFS0Provider extends ISuperProvider { + boolean isEncrypted(); + String getMagic(); + int getFilesCount(); + int getStringTableSize(); + byte[] getPadding(); + + PFS0subFile[] getPfs0subFiles(); +} diff --git a/src/main/java/libKonogonka/Tools/PFS0/PFS0EncryptedProvider.java b/src/main/java/libKonogonka/Tools/PFS0/PFS0EncryptedProvider.java new file mode 100644 index 0000000..53baae1 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/PFS0/PFS0EncryptedProvider.java @@ -0,0 +1,304 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.PFS0; + +import libKonogonka.ctraes.AesCtrDecryptSimple; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.*; + +public class PFS0EncryptedProvider implements IPFS0Provider{ + private long rawFileDataStart; // Always -1 @ PFS0EncryptedProvider + + private String magic; + private int filesCount; + private int stringTableSize; + private byte[] padding; + private PFS0subFile[] pfs0subFiles; + + //--------------------------------------- + + private long rawBlockDataStart; + + private long offsetPositionInFile; + private File file; + private byte[] key; + private byte[] sectionCTR; + private long mediaStartOffset; // In 512-blocks + private long mediaEndOffset; // In 512-blocks + + public PFS0EncryptedProvider(PipedInputStream pipedInputStream, + long pfs0offsetPosition, + long offsetPositionInFile, + File fileWithEncPFS0, + byte[] key, + byte[] sectionCTR, + long mediaStartOffset, + long mediaEndOffset + ) throws Exception{ + // Populate 'meta' data that is needed for getProviderSubFilePipedInpStream() + this.offsetPositionInFile = offsetPositionInFile; + this.file = fileWithEncPFS0; + this.key = key; + this.sectionCTR = sectionCTR; + this.mediaStartOffset = mediaStartOffset; + this.mediaEndOffset = mediaEndOffset; + // pfs0offsetPosition is a position relative to Media block. Lets add pfs0 'header's' bytes count and get raw data start position in media block + rawFileDataStart = -1; // Set -1 for PFS0EncryptedProvider + // Detect raw data start position using next var + rawBlockDataStart = pfs0offsetPosition; + + byte[] fileStartingBytes = new byte[0x10]; + // Read PFS0Provider, files count, header, padding (4 zero bytes) + + for (int i = 0; i < 0x10; i++){ + int currentByte = pipedInputStream.read(); + if (currentByte == -1) { + throw new Exception("PFS0EncryptedProvider: Reading stream suddenly ended while trying to read starting 0x10 bytes"); + } + fileStartingBytes[i] = (byte)currentByte; + } + // Update position + rawBlockDataStart += 0x10; + // Check PFS0Provider + magic = new String(fileStartingBytes, 0x0, 0x4, StandardCharsets.US_ASCII); + if (! magic.equals("PFS0")){ + throw new Exception("PFS0EncryptedProvider: Bad magic"); + } + // Get files count + filesCount = getLEint(fileStartingBytes, 0x4); + if (filesCount <= 0 ) { + throw new Exception("PFS0EncryptedProvider: Files count is too small"); + } + // Get string table + stringTableSize = getLEint(fileStartingBytes, 0x8); + if (stringTableSize <= 0 ){ + throw new Exception("PFS0EncryptedProvider: String table is too small"); + } + padding = Arrays.copyOfRange(fileStartingBytes, 0xc, 0x10); + //--------------------------------------------------------------------------------------------------------- + pfs0subFiles = new PFS0subFile[filesCount]; + + long[] offsetsSubFiles = new long[filesCount]; + long[] sizesSubFiles = new long[filesCount]; + int[] strTableOffsets = new int[filesCount]; + byte[][] zeroBytes = new byte[filesCount][]; + + byte[] fileEntryTable = new byte[0x18]; + for (int i=0; i < filesCount; i++){ + for (int j = 0; j < 0x18; j++){ + int currentByte = pipedInputStream.read(); + if (currentByte == -1) { + throw new Exception("PFS0EncryptedProvider: Reading stream suddenly ended while trying to read File Entry Table #"+i); + } + fileEntryTable[j] = (byte)currentByte; + } + offsetsSubFiles[i] = getLElong(fileEntryTable, 0); + sizesSubFiles[i] = getLElong(fileEntryTable, 0x8); + strTableOffsets[i] = getLEint(fileEntryTable, 0x10); + zeroBytes[i] = Arrays.copyOfRange(fileEntryTable, 0x14, 0x18); + // Update position + rawBlockDataStart += 0x18; + } + //********************************************************************************************************** + // In here pointer in front of String table + String[] subFileNames = new String[filesCount]; + byte[] stringTbl = new byte[stringTableSize]; + + for (int i = 0; i < stringTableSize; i++){ + int currentByte = pipedInputStream.read(); + if (currentByte == -1) { + throw new Exception("PFS0EncryptedProvider: Reading stream suddenly ended while trying to read string table"); + } + stringTbl[i] = (byte)currentByte; + } + // Update position + rawBlockDataStart += stringTableSize; + + for (int i=0; i < filesCount; i++){ + int j = 0; + while (stringTbl[strTableOffsets[i]+j] != (byte)0x00) + j++; + subFileNames[i] = new String(stringTbl, strTableOffsets[i], j, StandardCharsets.UTF_8); + } + for (int i = 0; i < filesCount; i++){ + pfs0subFiles[i] = new PFS0subFile( + subFileNames[i], + offsetsSubFiles[i], + sizesSubFiles[i], + zeroBytes[i] + ); + } + } + + @Override + public boolean isEncrypted() { return true; } + @Override + public String getMagic() { return magic; } + @Override + public int getFilesCount() { return filesCount; } + @Override + public int getStringTableSize() { return stringTableSize; } + @Override + public byte[] getPadding() { return padding; } + @Override + public long getRawFileDataStart() { return rawFileDataStart; } + @Override + public PFS0subFile[] getPfs0subFiles() { return pfs0subFiles; } + @Override + public File getFile(){ return file; } + @Override + public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception { // TODO: rewrite + if (subFileNumber >= pfs0subFiles.length) + throw new Exception("PFS0Provider -> getPfs0subFilePipedInpStream(): Requested sub file doesn't exists"); + + Thread workerThread; + PipedOutputStream streamOut = new PipedOutputStream(); + + PipedInputStream streamIn = new PipedInputStream(streamOut); + workerThread = new Thread(() -> { + System.out.println("PFS0EncryptedProvider -> getPfs0subFilePipedInpStream(): Executing thread"); + try { + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + // Let's store what we're about to skip + long skipInitL = offsetPositionInFile + (mediaStartOffset * 0x200); // NOTE: NEVER cast to int. + // Check if skip was successful + if (bis.skip(skipInitL) != skipInitL) { + System.out.println("PFS0EncryptedProvider -> getPfs0subFilePipedInpStream(): Failed to skip range "+skipInitL); + return; + } + + AesCtrDecryptSimple aesCtrDecryptSimple = new AesCtrDecryptSimple(key, sectionCTR, mediaStartOffset * 0x200); + + byte[] encryptedBlock; + byte[] dectyptedBlock; + + //----------------------------- Pre-set: skip non-necessary data -------------------------------- + + long startBlock = (rawBlockDataStart + pfs0subFiles[subFileNumber].getOffset()) / 0x200; // <- pointing to place where actual data starts + int skipBytes; + + if (startBlock > 0) { + aesCtrDecryptSimple.skipNext(startBlock); + skipBytes = (int)(startBlock * 0x200); + if (bis.skip(skipBytes) != skipBytes) { + System.out.println("PFS0EncryptedProvider -> getPfs0subFilePipedInpStream(): Failed to skip range "+skipBytes); + return; + } + } + + //----------------------------- Step 1: get starting bytes from the end of the junk block -------------------------------- + + // Since our data could be located in position with some offset from the decrypted block, let's skip bytes left. Considering the case when data is not aligned to block + skipBytes = (int) ( (rawBlockDataStart + pfs0subFiles[subFileNumber].getOffset()) - startBlock * 0x200); // <- How much bytes shall we skip to reach requested data start of sub-file + + if (skipBytes > 0) { + encryptedBlock = new byte[0x200]; + if (bis.read(encryptedBlock) == 0x200) { + dectyptedBlock = aesCtrDecryptSimple.dectyptNext(encryptedBlock); + // If we have extra-small file that is less then a block and even more + if ((0x200 - skipBytes) > pfs0subFiles[subFileNumber].getSize()){ + streamOut.write(dectyptedBlock, skipBytes, (int) pfs0subFiles[subFileNumber].getSize()); // safe cast + bis.close(); + streamOut.close(); + return; + } + else + streamOut.write(dectyptedBlock, skipBytes, 0x200 - skipBytes); + } + else { + System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): Unable to get 512 bytes from 1st bock"); + return; + } + startBlock++; + } + long endBlock = pfs0subFiles[subFileNumber].getSize() / 0x200 + startBlock; // <- pointing to place where any data related to this media-block ends + + //----------------------------- Step 2: Detect if we have junk data on the end of the final block -------------------------------- + int extraData = (int)(rawBlockDataStart+pfs0subFiles[subFileNumber].getOffset()+pfs0subFiles[subFileNumber].getSize() - (endBlock*0x200)); // safe cast + if (extraData < 0){ + endBlock--; + } + //----------------------------- Step 3: Read main part of data -------------------------------- + // Here we're reading main amount of bytes. We can read only less bytes. + while ( startBlock < endBlock) { + encryptedBlock = new byte[0x200]; + if (bis.read(encryptedBlock) == 0x200) { + //dectyptedBlock = aesCtr.decrypt(encryptedBlock); + dectyptedBlock = aesCtrDecryptSimple.dectyptNext(encryptedBlock); + // Writing decrypted data to pipe + streamOut.write(dectyptedBlock); + } + else { + System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): Unable to get 512 bytes from bock"); + return; + } + startBlock++; + } + //----------------------------- Step 4: Read what's left -------------------------------- + // Now we have to find out if data overlaps to one more extra block + if (extraData > 0){ // In case we didn't get what we want + encryptedBlock = new byte[0x200]; + if (bis.read(encryptedBlock) == 0x200) { + dectyptedBlock = aesCtrDecryptSimple.dectyptNext(encryptedBlock); + streamOut.write(dectyptedBlock, 0, extraData); + } + else { + System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): Unable to get 512 bytes from bock"); + return; + } + } + else if (extraData < 0){ // In case we can get more than we need + encryptedBlock = new byte[0x200]; + if (bis.read(encryptedBlock) == 0x200) { + dectyptedBlock = aesCtrDecryptSimple.dectyptNext(encryptedBlock); + streamOut.write(dectyptedBlock, 0, 0x200 + extraData); // WTF ??? THIS LOOKS INCORRECT + } + else { + System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): Unable to get 512 bytes from last bock"); + return; + } + } + bis.close(); + streamOut.close(); + } + catch (Exception e){ + System.out.println("PFS0EncryptedProvider -> getProviderSubFilePipedInpStream(): "+e.getMessage()); + e.printStackTrace(); + } + System.out.println("PFS0EncryptedProvider -> getPfs0subFilePipedInpStream(): Thread died"); + + + }); + workerThread.start(); + return streamIn; + } + + @Override + public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception{ + for (int i = 0; i < pfs0subFiles.length; i++){ + if (pfs0subFiles[i].getName().equals(subFileName)) + return getProviderSubFilePipedInpStream(i); + } + return null; + } +} diff --git a/src/main/java/libKonogonka/Tools/PFS0/PFS0Provider.java b/src/main/java/libKonogonka/Tools/PFS0/PFS0Provider.java new file mode 100644 index 0000000..3d266fb --- /dev/null +++ b/src/main/java/libKonogonka/Tools/PFS0/PFS0Provider.java @@ -0,0 +1,189 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.PFS0; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.*; + +public class PFS0Provider implements IPFS0Provider{ + private long rawFileDataStart; // Where data starts, excluding header, string table etc. + + private String magic; + private int filesCount; + private int stringTableSize; + private byte[] padding; + private PFS0subFile[] pfs0subFiles; + + private File file; + + public PFS0Provider(File fileWithPfs0) throws Exception{ this(fileWithPfs0, 0); } + + public PFS0Provider(File fileWithPfs0, long pfs0offsetPosition) throws Exception{ + file = fileWithPfs0; + RandomAccessFile raf = new RandomAccessFile(fileWithPfs0, "r"); // TODO: replace to bufferedInputStream + + raf.seek(pfs0offsetPosition); + byte[] fileStartingBytes = new byte[0x10]; + // Read PFS0Provider, files count, header, padding (4 zero bytes) + if (raf.read(fileStartingBytes) != 0x10){ + raf.close(); + throw new Exception("PFS0Provider: Unable to read starting bytes"); + } + // Check PFS0Provider + magic = new String(fileStartingBytes, 0x0, 0x4, StandardCharsets.US_ASCII); + if (! magic.equals("PFS0")){ + raf.close(); + throw new Exception("PFS0Provider: Bad magic"); + } + // Get files count + filesCount = getLEint(fileStartingBytes, 0x4); + if (filesCount <= 0 ) { + raf.close(); + throw new Exception("PFS0Provider: Files count is too small"); + } + // Get string table + stringTableSize = getLEint(fileStartingBytes, 0x8); + if (stringTableSize <= 0 ){ + raf.close(); + throw new Exception("PFS0Provider: String table is too small"); + } + padding = Arrays.copyOfRange(fileStartingBytes, 0xc, 0x10); + //--------------------------------------------------------------------------------------------------------- + pfs0subFiles = new PFS0subFile[filesCount]; + + long[] offsetsSubFiles = new long[filesCount]; + long[] sizesSubFiles = new long[filesCount]; + int[] strTableOffsets = new int[filesCount]; + byte[][] zeroBytes = new byte[filesCount][]; + + byte[] fileEntryTable = new byte[0x18]; + for (int i=0; i= pfs0subFiles.length) { + throw new Exception("PFS0Provider -> getPfs0subFilePipedInpStream(): Requested sub file doesn't exists"); + } + PipedOutputStream streamOut = new PipedOutputStream(); + Thread workerThread; + + PipedInputStream streamIn = new PipedInputStream(streamOut); + + workerThread = new Thread(() -> { + System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Executing thread"); + try { + long subFileRealPosition = rawFileDataStart + pfs0subFiles[subFileNumber].getOffset(); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + if (bis.skip(subFileRealPosition) != subFileRealPosition) { + System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Unable to skip requested offset"); + return; + } + + int readPice = 8388608; // 8mb NOTE: consider switching to 1mb 1048576 + + long readFrom = 0; + long realFileSize = pfs0subFiles[subFileNumber].getSize(); + + byte[] readBuf; + + while (readFrom < realFileSize) { + if (realFileSize - readFrom < readPice) + readPice = Math.toIntExact(realFileSize - readFrom); // it's safe, I guarantee + readBuf = new byte[readPice]; + if (bis.read(readBuf) != readPice) { + System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Unable to read requested size from file."); + return; + } + streamOut.write(readBuf); + readFrom += readPice; + } + bis.close(); + streamOut.close(); + } catch (IOException ioe) { + System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Unable to provide stream"); + ioe.printStackTrace(); + } + System.out.println("PFS0Provider -> getPfs0subFilePipedInpStream(): Thread died"); + }); + workerThread.start(); + return streamIn; + } + /** + * Some sugar + * */ + @Override + public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception { + for (int i = 0; i < pfs0subFiles.length; i++){ + if (pfs0subFiles[i].getName().equals(subFileName)) + return getProviderSubFilePipedInpStream(i); + } + return null; + } +} diff --git a/src/main/java/libKonogonka/Tools/PFS0/PFS0subFile.java b/src/main/java/libKonogonka/Tools/PFS0/PFS0subFile.java new file mode 100644 index 0000000..dc7df45 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/PFS0/PFS0subFile.java @@ -0,0 +1,38 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.PFS0; + +public class PFS0subFile { + private String name; + private long offset; // REAL in file (including offset in NCA/NSP file) + private long size; + private byte[] zeroes; + + public PFS0subFile(String name, long offset, long size, byte[] zeroesInTable){ + this.name = name; + this.offset = offset; + this.size = size; + this.zeroes = zeroesInTable; + } + + public String getName() { return name; } + public long getOffset() { return offset; } + public long getSize() { return size; } + public byte[] getZeroes() { return zeroes; } +} diff --git a/src/main/java/libKonogonka/Tools/RomFs/FileMeta4Debug.java b/src/main/java/libKonogonka/Tools/RomFs/FileMeta4Debug.java new file mode 100644 index 0000000..1684ed0 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/RomFs/FileMeta4Debug.java @@ -0,0 +1,87 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.RomFs; + +import libKonogonka.LoperConverter; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static libKonogonka.RainbowDump.formatDecHexString; + +public class FileMeta4Debug { + + List allFiles; + + FileMeta4Debug(long fileMetadataTableLength, byte[] fileMetadataTable) { + allFiles = new ArrayList<>(); + int i = 0; + while (i < fileMetadataTableLength) { + FileMeta fileMeta = new FileMeta(); + fileMeta.containingDirectoryOffset = LoperConverter.getLEint(fileMetadataTable, i); + i += 4; + fileMeta.nextSiblingFileOffset = LoperConverter.getLEint(fileMetadataTable, i); + i += 4; + fileMeta.fileDataOffset = LoperConverter.getLElong(fileMetadataTable, i); + i += 8; + fileMeta.fileDataLength = LoperConverter.getLElong(fileMetadataTable, i); + i += 8; + fileMeta.nextFileOffset = LoperConverter.getLEint(fileMetadataTable, i); + i += 4; + fileMeta.fileNameLength = LoperConverter.getLEint(fileMetadataTable, i); + i += 4; + fileMeta.fileName = new String(Arrays.copyOfRange(fileMetadataTable, i, i + fileMeta.fileNameLength), StandardCharsets.UTF_8); + ; + i += getRealNameSize(fileMeta.fileNameLength); + + allFiles.add(fileMeta); + } + + for (FileMeta fileMeta : allFiles){ + System.out.println( + "-------------------------FILE--------------------------------\n" + + "Offset of Containing Directory " + formatDecHexString(fileMeta.containingDirectoryOffset) + "\n" + + "Offset of next Sibling File " + formatDecHexString(fileMeta.nextSiblingFileOffset) + "\n" + + "Offset of File's Data " + formatDecHexString(fileMeta.fileDataOffset) + "\n" + + "Length of File's Data " + formatDecHexString(fileMeta.fileDataLength) + "\n" + + "Offset of next File in the same Hash Table bucket " + formatDecHexString(fileMeta.nextFileOffset) + "\n" + + "Name Length " + formatDecHexString(fileMeta.fileNameLength) + "\n" + + "Name Length (rounded up to multiple of 4) " + fileMeta.fileName + "\n" + ); + } + } + + private int getRealNameSize(int value){ + if (value % 4 == 0) + return value; + return value + 4 - value % 4; + } + + private static class FileMeta{ + int containingDirectoryOffset; + int nextSiblingFileOffset; + long fileDataOffset; + long fileDataLength; + int nextFileOffset; + int fileNameLength; + String fileName; + } +} diff --git a/src/main/java/libKonogonka/Tools/RomFs/FileSystemEntry.java b/src/main/java/libKonogonka/Tools/RomFs/FileSystemEntry.java new file mode 100644 index 0000000..fee7074 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/RomFs/FileSystemEntry.java @@ -0,0 +1,192 @@ +/* + * Copyright 2019-2020 Dmitry Isaenko + * + * This file is part of Konogonka. + * + * Konogonka 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. + * + * Konogonka 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 Konogonka. If not, see . + */ + +package libKonogonka.Tools.RomFs; + +import libKonogonka.LoperConverter; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class FileSystemEntry { + private boolean directoryFlag; + private String name; + private List content; + + private static byte[] dirsMetadataTable; + private static byte[] filesMetadataTable; + + private long fileOffset; + private long fileSize; + + public FileSystemEntry(byte[] dirsMetadataTable, byte[] filesMetadataTable) throws Exception{ + FileSystemEntry.dirsMetadataTable = dirsMetadataTable; + FileSystemEntry.filesMetadataTable = filesMetadataTable; + this.content = new ArrayList<>(); + this.directoryFlag = true; + DirectoryMetaData rootDirectoryMetaData = new DirectoryMetaData(); + if (rootDirectoryMetaData.dirName.isEmpty()) + this.name = "ROOT"; + else + this.name = rootDirectoryMetaData.dirName; + if (rootDirectoryMetaData.parentDirectoryOffset != 0) + throw new Exception("Offset of Parent Directory is incorrect. Expected 0 for root, received value is "+ rootDirectoryMetaData.parentDirectoryOffset); + if (rootDirectoryMetaData.nextSiblingDirectoryOffset != -1) + throw new Exception("Offset of next Sibling Directory is incorrect. Expected -1 for root, received value is "+ rootDirectoryMetaData.nextSiblingDirectoryOffset); + if (rootDirectoryMetaData.firstSubdirectoryOffset != -1) + content.add(getDirectory(rootDirectoryMetaData.firstSubdirectoryOffset)); + if (rootDirectoryMetaData.firstFileOffset != -1) + content.add(getFile(this, rootDirectoryMetaData.firstFileOffset)); + content.sort(Comparator.comparingLong(FileSystemEntry::getFileOffset)); + } + + private FileSystemEntry(){ + this.content = new ArrayList<>(); + } + + private FileSystemEntry getDirectory(int childDirMetaPosition){ + FileSystemEntry fileSystemEntry = new FileSystemEntry(); + fileSystemEntry.directoryFlag = true; + + DirectoryMetaData directoryMetaData = new DirectoryMetaData(childDirMetaPosition); + fileSystemEntry.name = directoryMetaData.dirName; + + if (directoryMetaData.nextSiblingDirectoryOffset != -1) + this.content.add(getDirectory(directoryMetaData.nextSiblingDirectoryOffset)); + + if (directoryMetaData.firstSubdirectoryOffset != -1) + fileSystemEntry.content.add(getDirectory(directoryMetaData.firstSubdirectoryOffset)); + + if (directoryMetaData.firstFileOffset != -1) + fileSystemEntry.content.add(getFile(fileSystemEntry, directoryMetaData.firstFileOffset)); + + fileSystemEntry.content.sort(Comparator.comparingLong(FileSystemEntry::getFileOffset)); + + return fileSystemEntry; + } + + private FileSystemEntry getFile(FileSystemEntry directoryContainer, int childFileMetaPosition){ + FileSystemEntry fileSystemEntry = new FileSystemEntry(); + fileSystemEntry.directoryFlag = false; + + FileMetaData fileMetaData = new FileMetaData(childFileMetaPosition); + fileSystemEntry.name = fileMetaData.fileName; + fileSystemEntry.fileOffset = fileMetaData.fileDataRealOffset; + fileSystemEntry.fileSize = fileMetaData.fileDataRealLength; + if (fileMetaData.nextSiblingFileOffset != -1) + directoryContainer.content.add( getFile(directoryContainer, fileMetaData.nextSiblingFileOffset) ); + + return fileSystemEntry; + } + + public boolean isDirectory() { return directoryFlag; } + public boolean isFile() { return ! directoryFlag; } + public long getFileOffset() { return fileOffset; } + public long getFileSize() { return fileSize; } + public List getContent() { return content; } + public String getName(){ return name; } + + + private static class DirectoryMetaData { + private int parentDirectoryOffset; + private int nextSiblingDirectoryOffset; + private int firstSubdirectoryOffset; + private int firstFileOffset; + + private String dirName; + + private DirectoryMetaData(){ + this(0); + } + private DirectoryMetaData(int childDirMetaPosition){ + int i = childDirMetaPosition; + parentDirectoryOffset = LoperConverter.getLEint(dirsMetadataTable, i); + i += 4; + nextSiblingDirectoryOffset = LoperConverter.getLEint(dirsMetadataTable, i); + i += 4; + firstSubdirectoryOffset = LoperConverter.getLEint(dirsMetadataTable, i); + i += 4; + firstFileOffset = LoperConverter.getLEint(dirsMetadataTable, i); + i += 4; + // int nextHashTableBucketDirectoryOffset = LoperConverter.getLEint(dirsMetadataTable, i); + i += 4; + int dirNameLength = LoperConverter.getLEint(dirsMetadataTable, i); + i += 4; + dirName = new String(Arrays.copyOfRange(dirsMetadataTable, i, i + dirNameLength), StandardCharsets.UTF_8); + //i += getRealNameSize(dirNameLength); + } + + private int getRealNameSize(int value){ + if (value % 4 == 0) + return value; + return value + 4 - value % 4; + } + } + private static class FileMetaData { + + private int nextSiblingFileOffset; + private long fileDataRealOffset; + private long fileDataRealLength; + + private String fileName; + + private FileMetaData(){ + this(0); + } + + private FileMetaData(int childFileMetaPosition){ + int i = childFileMetaPosition; + // int containingDirectoryOffset = LoperConverter.getLEint(filesMetadataTable, i); // never used + i += 4; + nextSiblingFileOffset = LoperConverter.getLEint(filesMetadataTable, i); + i += 4; + fileDataRealOffset = LoperConverter.getLElong(filesMetadataTable, i); + i += 8; + fileDataRealLength = LoperConverter.getLElong(filesMetadataTable, i); + i += 8; + //int nextHashTableBucketFileOffset = LoperConverter.getLEint(filesMetadataTable, i); + i += 4; + int fileNameLength = LoperConverter.getLEint(filesMetadataTable, i); + i += 4; + fileName = new String(Arrays.copyOfRange(filesMetadataTable, i, i + fileNameLength), StandardCharsets.UTF_8);; + //i += getRealNameSize(fileNameLength); + } + } + + public void printTreeForDebug(){ + System.out.println("/"); + for (FileSystemEntry entry: content) + printEntry(2, entry); + } + private void printEntry(int cnt, FileSystemEntry entry) { + for (int i = 0; i < cnt; i++) + System.out.print(" "); + + if (entry.isDirectory()){ + System.out.println("|-" + entry.getName()); + for (FileSystemEntry e : entry.content) + printEntry(cnt+2, e); + } + else + System.out.println("|-" + entry.getName() + String.format(" 0x%-10x 0x%-10x", entry.fileOffset, entry.fileSize)); + } +} diff --git a/src/main/java/libKonogonka/Tools/RomFs/FolderMeta4Debug.java b/src/main/java/libKonogonka/Tools/RomFs/FolderMeta4Debug.java new file mode 100644 index 0000000..d23adfb --- /dev/null +++ b/src/main/java/libKonogonka/Tools/RomFs/FolderMeta4Debug.java @@ -0,0 +1,84 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.RomFs; + +import libKonogonka.LoperConverter; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static libKonogonka.RainbowDump.formatDecHexString; + +public class FolderMeta4Debug { + + List allFolders; + + FolderMeta4Debug(long directoryMetadataTableLength, byte[] directoryMetadataTable){ + allFolders = new ArrayList<>(); + int i = 0; + while (i < directoryMetadataTableLength){ + FolderMeta folderMeta = new FolderMeta(); + folderMeta.parentDirectoryOffset = LoperConverter.getLEint(directoryMetadataTable, i); + i += 4; + folderMeta.nextSiblingDirectoryOffset = LoperConverter.getLEint(directoryMetadataTable, i); + i += 4; + folderMeta.firstSubdirectoryOffset = LoperConverter.getLEint(directoryMetadataTable, i); + i += 4; + folderMeta.firstFileOffset = LoperConverter.getLEint(directoryMetadataTable, i); + i += 4; + folderMeta.nextDirectoryOffset = LoperConverter.getLEint(directoryMetadataTable, i); + i += 4; + folderMeta.dirNameLength = LoperConverter.getLEint(directoryMetadataTable, i); + i += 4; + folderMeta.dirName = new String(Arrays.copyOfRange(directoryMetadataTable, i, i + folderMeta.dirNameLength), StandardCharsets.UTF_8); + i += getRealNameSize(folderMeta.dirNameLength); + + System.out.println( + "---------------------------DIRECTORY------------------------\n" + + "Offset of Parent Directory (self if Root) " + formatDecHexString(folderMeta.parentDirectoryOffset ) +"\n" + + "Offset of next Sibling Directory " + formatDecHexString(folderMeta.nextSiblingDirectoryOffset) +"\n" + + "Offset of first Child Directory (Subdirectory) " + formatDecHexString(folderMeta.firstSubdirectoryOffset ) +"\n" + + "Offset of first File (in File Metadata Table) " + formatDecHexString(folderMeta.firstFileOffset ) +"\n" + + "Offset of next Directory in the same Hash Table bucket " + formatDecHexString(folderMeta.nextDirectoryOffset ) +"\n" + + "Name Length " + formatDecHexString(folderMeta.dirNameLength ) +"\n" + + "Name Length (rounded up to multiple of 4) " + folderMeta.dirName + "\n" + ); + + allFolders.add(folderMeta); + } + } + + private int getRealNameSize(int value){ + if (value % 4 == 0) + return value; + return value + 4 - value % 4; + } + + private static class FolderMeta { + int parentDirectoryOffset; + int nextSiblingDirectoryOffset; + int firstSubdirectoryOffset; + int firstFileOffset; + int nextDirectoryOffset; + int dirNameLength; + String dirName; + } +} diff --git a/src/main/java/libKonogonka/Tools/RomFs/IRomFsProvider.java b/src/main/java/libKonogonka/Tools/RomFs/IRomFsProvider.java new file mode 100644 index 0000000..2883a83 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/RomFs/IRomFsProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019-2020 Dmitry Isaenko + * + * This file is part of Konogonka. + * + * Konogonka 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. + * + * Konogonka 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 Konogonka. If not, see . + */ + +package libKonogonka.Tools.RomFs; + +import java.io.File; +import java.io.PipedInputStream; + +public interface IRomFsProvider { + long getLevel6Offset(); + Level6Header getHeader(); + FileSystemEntry getRootEntry(); + PipedInputStream getContent(FileSystemEntry entry) throws Exception; + File getFile(); +} diff --git a/src/main/java/libKonogonka/Tools/RomFs/Level6Header.java b/src/main/java/libKonogonka/Tools/RomFs/Level6Header.java new file mode 100644 index 0000000..1b6bfe1 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/RomFs/Level6Header.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019-2020 Dmitry Isaenko + * + * This file is part of Konogonka. + * + * Konogonka 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. + * + * Konogonka 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 Konogonka. If not, see . + */ + +package libKonogonka.Tools.RomFs; + +import libKonogonka.LoperConverter; +import libKonogonka.RainbowDump; + +import java.util.Arrays; + +public class Level6Header { + private long headerLength; + private long directoryHashTableOffset; + private long directoryHashTableLength; + private long directoryMetadataTableOffset; + private long directoryMetadataTableLength; + private long fileHashTableOffset; + private long fileHashTableLength; + private long fileMetadataTableOffset; + private long fileMetadataTableLength; + private long fileDataOffset; + + private byte[] headerBytes; + private int i; + + Level6Header(byte[] headerBytes) throws Exception{ + this.headerBytes = headerBytes; + if (headerBytes.length < 0x50) + throw new Exception("Level 6 Header section is too small"); + headerLength = getNext(); + directoryHashTableOffset = getNext(); + directoryHashTableOffset <<= 32; + directoryHashTableLength = getNext(); + directoryMetadataTableOffset = getNext(); + directoryMetadataTableLength = getNext(); + fileHashTableOffset = getNext(); + fileHashTableLength = getNext(); + fileMetadataTableOffset = getNext(); + fileMetadataTableLength = getNext(); + fileDataOffset = getNext(); + RainbowDump.hexDumpUTF8(Arrays.copyOfRange(headerBytes, 0, 0x50)); + } + + private long getNext(){ + final long result = LoperConverter.getLEint(headerBytes, i); + i += 0x8; + return result; + } + + public long getHeaderLength() { return headerLength; } + public long getDirectoryHashTableOffset() { return directoryHashTableOffset; } + public long getDirectoryHashTableLength() { return directoryHashTableLength; } + public long getDirectoryMetadataTableOffset() { return directoryMetadataTableOffset; } + public long getDirectoryMetadataTableLength() { return directoryMetadataTableLength; } + public long getFileHashTableOffset() { return fileHashTableOffset; } + public long getFileHashTableLength() { return fileHashTableLength; } + public long getFileMetadataTableOffset() { return fileMetadataTableOffset; } + public long getFileMetadataTableLength() { return fileMetadataTableLength; } + public long getFileDataOffset() { return fileDataOffset; } + + public void printDebugInfo(){ + System.out.println("== Level 6 Header ==\n" + + "Header Length (always 0x50 ?) "+ RainbowDump.formatDecHexString(headerLength)+" (size of this structure within first 0x200 block of LEVEL 6 part)\n" + + "Directory Hash Table Offset "+ RainbowDump.formatDecHexString(directoryHashTableOffset)+" (against THIS block where HEADER contains)\n" + + "Directory Hash Table Length "+ RainbowDump.formatDecHexString(directoryHashTableLength) + "\n" + + "Directory Metadata Table Offset "+ RainbowDump.formatDecHexString(directoryMetadataTableOffset) + "\n" + + "Directory Metadata Table Length "+ RainbowDump.formatDecHexString(directoryMetadataTableLength) + "\n" + + "File Hash Table Offset "+ RainbowDump.formatDecHexString(fileHashTableOffset) + "\n" + + "File Hash Table Length "+ RainbowDump.formatDecHexString(fileHashTableLength) + "\n" + + "File Metadata Table Offset "+ RainbowDump.formatDecHexString(fileMetadataTableOffset) + "\n" + + "File Metadata Table Length "+ RainbowDump.formatDecHexString(fileMetadataTableLength) + "\n" + + "File Data Offset "+ RainbowDump.formatDecHexString(fileDataOffset) + "\n" + + "-------------------------------------------------------------" + ); + } +} diff --git a/src/main/java/libKonogonka/Tools/RomFs/RomFsDecryptedProvider.java b/src/main/java/libKonogonka/Tools/RomFs/RomFsDecryptedProvider.java new file mode 100644 index 0000000..9dbbb81 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/RomFs/RomFsDecryptedProvider.java @@ -0,0 +1,166 @@ +/* + * Copyright 2019-2020 Dmitry Isaenko + * + * This file is part of Konogonka. + * + * Konogonka 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. + * + * Konogonka 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 Konogonka. If not, see . + */ + +package libKonogonka.Tools.RomFs; + +import java.io.*; + +public class RomFsDecryptedProvider implements IRomFsProvider{ + + private long level6Offset; + + private File file; + private Level6Header header; + + private FileSystemEntry rootEntry; + + public RomFsDecryptedProvider(File decryptedFsImageFile, long level6Offset) throws Exception{ + if (level6Offset < 0) + throw new Exception("Incorrect Level 6 Offset"); + + this.file = decryptedFsImageFile; + + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(decryptedFsImageFile)); + + this.level6Offset = level6Offset; + + skipBytes(bis, level6Offset); + + byte[] rawDataChunk = new byte[0x50]; + + if (bis.read(rawDataChunk) != 0x50) + throw new Exception("Failed to read header (0x50)"); + + this.header = new Level6Header(rawDataChunk); + /* + // Print Dir Hash table as is: + long seekTo = header.getDirectoryHashTableOffset() - 0x50; + rawDataChunk = new byte[(int) header.getDirectoryHashTableLength()]; + skipTo(bis, seekTo); + if (bis.read(rawDataChunk) != (int) header.getDirectoryHashTableLength()) + throw new Exception("Failed to read Dir hash table"); + RainbowDump.hexDumpUTF8(rawDataChunk); + // Print Files Hash table as is: + seekTo = header.getFileHashTableOffset() - header.getDirectoryMetadataTableOffset(); + rawDataChunk = new byte[(int) header.getFileHashTableLength()]; + skipTo(bis, seekTo); + if (bis.read(rawDataChunk) != (int) header.getFileHashTableLength()) + throw new Exception("Failed to read Files hash table"); + RainbowDump.hexDumpUTF8(rawDataChunk); + */ + // Read directories metadata + long locationInFile = header.getDirectoryMetadataTableOffset() - 0x50; + + skipBytes(bis, locationInFile); + + if (header.getDirectoryMetadataTableLength() < 0) + throw new Exception("Not supported operation."); + + byte[] directoryMetadataTable = new byte[(int) header.getDirectoryMetadataTableLength()]; + + if (bis.read(directoryMetadataTable) != (int) header.getDirectoryMetadataTableLength()) + throw new Exception("Failed to read "+header.getDirectoryMetadataTableLength()); + // Read files metadata + locationInFile = header.getFileMetadataTableOffset() - header.getFileHashTableOffset(); // TODO: replace to 'CurrentPosition'? + + skipBytes(bis, locationInFile); + + if (header.getFileMetadataTableLength() < 0) + throw new Exception("Not supported operation."); + + byte[] fileMetadataTable = new byte[(int) header.getFileMetadataTableLength()]; + + if (bis.read(fileMetadataTable) != (int) header.getFileMetadataTableLength()) + throw new Exception("Failed to read "+header.getFileMetadataTableLength()); + + rootEntry = new FileSystemEntry(directoryMetadataTable, fileMetadataTable); + //printDebug(directoryMetadataTable, fileMetadataTable); + bis.close(); + } + private void skipBytes(BufferedInputStream bis, long size) throws Exception{ + long mustSkip = size; + long skipped = 0; + while (mustSkip > 0){ + skipped += bis.skip(mustSkip); + mustSkip = size - skipped; + } + } + @Override + public long getLevel6Offset() { return level6Offset; } + @Override + public Level6Header getHeader() { return header; } + @Override + public FileSystemEntry getRootEntry() { return rootEntry; } + @Override + public PipedInputStream getContent(FileSystemEntry entry) throws Exception{ + if (entry.isDirectory()) + throw new Exception("Request of the binary stream for the folder entry doesn't make sense."); + + PipedOutputStream streamOut = new PipedOutputStream(); + Thread workerThread; + + PipedInputStream streamIn = new PipedInputStream(streamOut); + + workerThread = new Thread(() -> { + System.out.println("RomFsDecryptedProvider -> getContent(): Executing thread"); + try { + long subFileRealPosition = level6Offset + header.getFileDataOffset() + entry.getFileOffset(); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + skipBytes(bis, subFileRealPosition); + + int readPice = 8388608; // 8mb NOTE: consider switching to 1mb 1048576 + + long readFrom = 0; + long realFileSize = entry.getFileSize(); + + byte[] readBuf; + + while (readFrom < realFileSize) { + if (realFileSize - readFrom < readPice) + readPice = Math.toIntExact(realFileSize - readFrom); // it's safe, I guarantee + readBuf = new byte[readPice]; + if (bis.read(readBuf) != readPice) { + System.out.println("RomFsDecryptedProvider -> getContent(): Unable to read requested size from file."); + return; + } + streamOut.write(readBuf); + readFrom += readPice; + } + bis.close(); + streamOut.close(); + } catch (Exception e) { + System.out.println("RomFsDecryptedProvider -> getContent(): Unable to provide stream"); + e.printStackTrace(); + } + System.out.println("RomFsDecryptedProvider -> getContent(): Thread is dead"); + }); + workerThread.start(); + return streamIn; + } + @Override + public File getFile() { + return file; + } + + private void printDebug(byte[] directoryMetadataTable, byte[] fileMetadataTable){ + new FolderMeta4Debug(header.getDirectoryMetadataTableLength(), directoryMetadataTable); + new FileMeta4Debug(header.getFileMetadataTableLength(), fileMetadataTable); + rootEntry.printTreeForDebug(); + } +} diff --git a/src/main/java/libKonogonka/Tools/RomFs/RomFsEncryptedProvider.java b/src/main/java/libKonogonka/Tools/RomFs/RomFsEncryptedProvider.java new file mode 100644 index 0000000..82aef57 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/RomFs/RomFsEncryptedProvider.java @@ -0,0 +1,292 @@ +/* + * Copyright 2019-2020 Dmitry Isaenko + * + * This file is part of Konogonka. + * + * Konogonka 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. + * + * Konogonka 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 Konogonka. If not, see . + */ + +package libKonogonka.Tools.RomFs; + +import libKonogonka.ctraes.AesCtrDecryptSimple; + +import java.io.File; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.io.RandomAccessFile; +import java.util.Arrays; + +public class RomFsEncryptedProvider implements IRomFsProvider{ + + private long level6Offset; + + private File file; + private Level6Header header; + + private FileSystemEntry rootEntry; + + //-------------------------------- + + private long romFSoffsetPosition; + private byte[] key; + private byte[] sectionCTR; + private long mediaStartOffset; + private long mediaEndOffset; + + public RomFsEncryptedProvider(long romFSoffsetPosition, + long level6Offset, + File fileWithEncPFS0, + byte[] key, + byte[] sectionCTR, + long mediaStartOffset, + long mediaEndOffset + ) throws Exception{ + this.file = fileWithEncPFS0; + this.level6Offset = level6Offset; + this.romFSoffsetPosition = romFSoffsetPosition; + this.key = key; + this.sectionCTR = sectionCTR; + this.mediaStartOffset = mediaStartOffset; + this.mediaEndOffset = mediaEndOffset; + + RandomAccessFile raf = new RandomAccessFile(file, "r"); + long abosluteOffsetPosition = romFSoffsetPosition + (mediaStartOffset * 0x200); + raf.seek(abosluteOffsetPosition + level6Offset); + + AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(key, sectionCTR, mediaStartOffset * 0x200); + //Go to Level 6 header + decryptor.skipNext(level6Offset / 0x200); + + // Decrypt data + byte[] encryptedBlock = new byte[0x200]; + byte[] dectyptedBlock; + if (raf.read(encryptedBlock) == 0x200) + dectyptedBlock = decryptor.dectyptNext(encryptedBlock); + else + throw new Exception("Failed to read header header (0x200 - block)"); + + this.header = new Level6Header(dectyptedBlock); + + header.printDebugInfo(); + + if (header.getDirectoryMetadataTableLength() < 0) + throw new Exception("Not supported: DirectoryMetadataTableLength < 0"); + + if (header.getFileMetadataTableLength() < 0) + throw new Exception("Not supported: FileMetadataTableLength < 0"); + + /*---------------------------------*/ + + // Read directories metadata + byte[] directoryMetadataTable = readMetaTable(abosluteOffsetPosition, + header.getDirectoryMetadataTableOffset(), + header.getDirectoryMetadataTableLength(), + raf); + + // Read files metadata + byte[] fileMetadataTable = readMetaTable(abosluteOffsetPosition, + header.getFileMetadataTableOffset(), + header.getFileMetadataTableLength(), + raf); + + rootEntry = new FileSystemEntry(directoryMetadataTable, fileMetadataTable); + + raf.close(); + } + + private byte[] readMetaTable(long abosluteOffsetPosition, + long metaOffset, + long metaSize, + RandomAccessFile raf) throws Exception{ + byte[] encryptedBlock; + byte[] dectyptedBlock; + byte[] metadataTable = new byte[(int) metaSize]; + //0 + AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(key, sectionCTR, mediaStartOffset * 0x200); + + long startBlock = metaOffset / 0x200; + + decryptor.skipNext(level6Offset / 0x200 + startBlock); + + raf.seek(abosluteOffsetPosition + level6Offset + startBlock * 0x200); + + //1 + long ignoreBytes = metaOffset - startBlock * 0x200; + long currentPosition = 0; + + if (ignoreBytes > 0) { + encryptedBlock = new byte[0x200]; + if (raf.read(encryptedBlock) == 0x200) { + dectyptedBlock = decryptor.dectyptNext(encryptedBlock); + // If we have extra-small file that is less then a block and even more + if ((0x200 - ignoreBytes) > metaSize){ + metadataTable = Arrays.copyOfRange(dectyptedBlock, (int)ignoreBytes, 0x200); + return metadataTable; + } + else { + System.arraycopy(dectyptedBlock, (int) ignoreBytes, metadataTable, 0, 0x200 - (int) ignoreBytes); + currentPosition = 0x200 - ignoreBytes; + } + } + else { + throw new Exception("RomFsEncryptedProvider(): Unable to get 512 bytes from 1st bock for Directory Metadata Table"); + } + startBlock++; + } + long endBlock = (metaSize + ignoreBytes) / 0x200 + startBlock; // <- pointing to place where any data related to this media-block ends + + //2 + int extraData = (int) ((endBlock - startBlock)*0x200 - (metaSize + ignoreBytes)); + + if (extraData < 0) + endBlock--; + //3 + while ( startBlock < endBlock ) { + encryptedBlock = new byte[0x200]; + if (raf.read(encryptedBlock) == 0x200) { + dectyptedBlock = decryptor.dectyptNext(encryptedBlock); + System.arraycopy(dectyptedBlock, 0, metadataTable, (int) currentPosition, 0x200); + } + else + throw new Exception("RomFsEncryptedProvider(): Unable to get 512 bytes from block for Directory Metadata Table"); + + startBlock++; + currentPosition += 0x200; + } + + //4 + if (extraData != 0){ // In case we didn't get what we want + encryptedBlock = new byte[0x200]; + if (raf.read(encryptedBlock) == 0x200) { + dectyptedBlock = decryptor.dectyptNext(encryptedBlock); + System.arraycopy(dectyptedBlock, 0, metadataTable, (int) currentPosition, Math.abs(extraData)); + } + else + throw new Exception("RomFsEncryptedProvider(): Unable to get 512 bytes from block for Directory Metadata Table"); + } + + return metadataTable; + } + + @Override + public long getLevel6Offset() { return level6Offset; } + @Override + public Level6Header getHeader() { return header; } + @Override + public FileSystemEntry getRootEntry() { return rootEntry; } + @Override + public PipedInputStream getContent(FileSystemEntry entry) throws Exception{ + if (entry.isDirectory()) + throw new Exception("Request of the binary stream for the folder entry doesn't make sense."); + + PipedOutputStream streamOut = new PipedOutputStream(); + Thread workerThread; + + PipedInputStream streamIn = new PipedInputStream(streamOut); + workerThread = new Thread(() -> { + System.out.println("RomFsDecryptedProvider -> getContent(): Executing thread"); + try { + + byte[] encryptedBlock; + byte[] dectyptedBlock; + + RandomAccessFile raf = new RandomAccessFile(file, "r"); + + //0 + AesCtrDecryptSimple decryptor = new AesCtrDecryptSimple(key, sectionCTR, mediaStartOffset * 0x200); + + long startBlock = (entry.getFileOffset() + header.getFileDataOffset()) / 0x200; + + decryptor.skipNext(level6Offset / 0x200 + startBlock); + + long abosluteOffsetPosition = romFSoffsetPosition + (mediaStartOffset * 0x200); + + raf.seek(abosluteOffsetPosition + level6Offset + startBlock * 0x200); + + //1 + long ignoreBytes = (entry.getFileOffset() + header.getFileDataOffset()) - startBlock * 0x200; + + if (ignoreBytes > 0) { + encryptedBlock = new byte[0x200]; + if (raf.read(encryptedBlock) == 0x200) { + dectyptedBlock = decryptor.dectyptNext(encryptedBlock); + // If we have extra-small file that is less then a block and even more + if ((0x200 - ignoreBytes) > entry.getFileSize()){ + streamOut.write(dectyptedBlock, (int)ignoreBytes, (int) entry.getFileSize()); // safe cast + raf.close(); + streamOut.close(); + return; + } + else { + streamOut.write(dectyptedBlock, (int) ignoreBytes, 0x200 - (int) ignoreBytes); + } + } + else { + throw new Exception("RomFsEncryptedProvider(): Unable to get 512 bytes from 1st bock for Directory Metadata Table"); + } + startBlock++; + } + long endBlock = (entry.getFileSize() + ignoreBytes) / 0x200 + startBlock; // <- pointing to place where any data related to this media-block ends + + //2 + int extraData = (int) ((endBlock - startBlock)*0x200 - (entry.getFileSize() + ignoreBytes)); + + if (extraData < 0) + endBlock--; + //3 + while ( startBlock < endBlock ) { + encryptedBlock = new byte[0x200]; + if (raf.read(encryptedBlock) == 0x200) { + dectyptedBlock = decryptor.dectyptNext(encryptedBlock); + streamOut.write(dectyptedBlock); + } + else + throw new Exception("RomFsEncryptedProvider(): Unable to get 512 bytes from block for Directory Metadata Table"); + + startBlock++; + } + + //4 + if (extraData != 0){ // In case we didn't get what we want + encryptedBlock = new byte[0x200]; + if (raf.read(encryptedBlock) == 0x200) { + dectyptedBlock = decryptor.dectyptNext(encryptedBlock); + streamOut.write(dectyptedBlock, 0, Math.abs(extraData)); + } + else + throw new Exception("RomFsEncryptedProvider(): Unable to get 512 bytes from block for Directory Metadata Table"); + } + raf.close(); + streamOut.close(); + } catch (Exception e) { + System.out.println("RomFsDecryptedProvider -> getContent(): Unable to provide stream"); + e.printStackTrace(); + } + System.out.println("RomFsDecryptedProvider -> getContent(): Thread is dead"); + }); + workerThread.start(); + return streamIn; + } + + @Override + public File getFile() { + return file; + } + + private void printDebug(byte[] directoryMetadataTable, byte[] fileMetadataTable){ + new FolderMeta4Debug(header.getDirectoryMetadataTableLength(), directoryMetadataTable); + new FileMeta4Debug(header.getFileMetadataTableLength(), fileMetadataTable); + rootEntry.printTreeForDebug(); + } +} diff --git a/src/main/java/libKonogonka/Tools/TIK/TIKProvider.java b/src/main/java/libKonogonka/Tools/TIK/TIKProvider.java new file mode 100644 index 0000000..e42a5ae --- /dev/null +++ b/src/main/java/libKonogonka/Tools/TIK/TIKProvider.java @@ -0,0 +1,181 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.TIK; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.*; +/* + +DON'T TRUST WIKI. Ticket size always (?) equal 0x02c0 (704 bytes) + +File structure byte-by-byte parsing + +Starting: +0x4 - Signature type +Signature type == 00 00 01 00 ? +Next: +0x200 - Signature size +Signature type == 01 00 01 00 ? +0x100 - Signature size +Signature type == 02 00 01 00 ? +0x3c - Signature size +Signature type == 03 00 01 00 ? +0x200 - Signature size +Signature type == 04 00 01 00 ? +0x100 - Signature size +Signature type == 05 00 01 00 ? +0x3c - Signature size +Next: +Signature type == 01 00 01 00 ? +0x3c - padding +Signature type == 01 00 01 00 ? +0x3c - padding +Signature type == 02 00 01 00 ? +0x40 - padding +Signature type == 03 00 01 00 ? +0c3c - padding +Signature type == 04 00 01 00 ? +0c3c - padding +Signature type == 05 00 01 00 ? +0x40 - padding +Next: +0x02c0 - Signature data ????? WTF? MUST BE AND IMPLEMENTED AS 0x180 + */ +/** + * TIKProvider is not a container, thus not a content-provider but provider of values-only + * */ +public class TIKProvider { + // Signature-related + private byte[] sigType; + private byte[] signature; + // Ticket + private String Issuer; + private byte[] TitleKeyBlockStartingBytes; // Actually 32 bytes. + private byte[] TitleKeyBlockEndingBytes; // Anything else + private byte Unknown1; + private byte TitleKeyType; + private byte[] Unknown2; + private byte MasterKeyRevision; + private byte[] Unknown3; + private byte[] TicketId; + private byte[] DeviceId; + private byte[] RightsId; + private byte[] RightsIdEndingBytes; + private byte[] AccountId; + private byte[] Unknown4; + + public TIKProvider(File file) throws Exception{ this(file, 0); } + + public TIKProvider(File file, long offset) throws Exception { + + if (file.length() - offset < 0x02c0) + throw new Exception("TIKProvider: File is too small."); + + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + if (bis.skip(offset) != offset) { + bis.close(); + throw new Exception("TIKProvider: Unable to skip requested range - " + offset); + } + + sigType = new byte[0x4]; + if (bis.read(sigType) != 4) { + bis.close(); + throw new Exception("TIKProvider: Unable to read requested range - " + offset); + } + + byte[] readChunk; + + switch (getLEint(sigType, 0)){ + case 65536: // RSA_4096 SHA1 + case 65539: // RSA_4096 SHA256 + readChunk = new byte[0x23c]; + if (bis.read(readChunk) != 0x23c) { + bis.close(); + throw new Exception("TIKProvider: Unable to read requested range - 0x23c"); + } + signature = Arrays.copyOfRange(readChunk, 0, 0x200); + break; + case 65537: // RSA_2048 SHA1 + case 65540: // RSA_2048 SHA256 + readChunk = new byte[0x13c]; + if (bis.read(readChunk) != 0x13c) { + bis.close(); + throw new Exception("TIKProvider: Unable to read requested range - 0x13c"); + } + signature = Arrays.copyOfRange(readChunk, 0, 0x100); + break; + case 65538: // ECDSA SHA1 + case 65541: // ECDSA SHA256 + readChunk = new byte[0x7c]; + if (bis.read(readChunk) != 0x7c) { + bis.close(); + throw new Exception("TIKProvider: Unable to read requested range - 0x7c"); + } + signature = Arrays.copyOfRange(readChunk, 0, 0x3c); + break; + default: + bis.close(); + throw new Exception("TIKProvider: Unknown ticket (Signature) type. Aborting."); + } + // Let's read ticket body itself + readChunk = new byte[0x180]; + + if (bis.read(readChunk) != 0x180) { + bis.close(); + throw new Exception("TIKProvider: Unable to read requested range - Ticket data"); + } + bis.close(); + + Issuer = new String(readChunk, 0, 0x40, StandardCharsets.UTF_8); + TitleKeyBlockStartingBytes = Arrays.copyOfRange(readChunk, 0x40, 0x50); + TitleKeyBlockEndingBytes = Arrays.copyOfRange(readChunk, 0x50, 0x140); + Unknown1 = readChunk[0x140]; + TitleKeyType = readChunk[0x141]; + Unknown2 = Arrays.copyOfRange(readChunk, 0x142, 0x145); + MasterKeyRevision = readChunk[0x145]; + Unknown3 = Arrays.copyOfRange(readChunk, 0x146, 0x150); + TicketId = Arrays.copyOfRange(readChunk, 0x150, 0x158); + DeviceId = Arrays.copyOfRange(readChunk, 0x158, 0x160); + RightsId = Arrays.copyOfRange(readChunk, 0x160, 0x170); + AccountId = Arrays.copyOfRange(readChunk,0x170, 0x174); + Unknown4 = Arrays.copyOfRange(readChunk, 0x174, 0x180); + } + + public byte[] getSigType() { return sigType; } + public byte[] getSignature() { return signature; } + + public String getIssuer() { return Issuer; } + public byte[] getTitleKeyBlockStartingBytes() { return TitleKeyBlockStartingBytes; } + public byte[] getTitleKeyBlockEndingBytes() { return TitleKeyBlockEndingBytes; } + public byte getUnknown1() { return Unknown1; } + public byte getTitleKeyType() { return TitleKeyType; } + public byte[] getUnknown2() { return Unknown2; } + public byte getMasterKeyRevision() { return MasterKeyRevision; } + public byte[] getUnknown3() { return Unknown3; } + public byte[] getTicketId() { return TicketId; } + public byte[] getDeviceId() { return DeviceId; } + public byte[] getRightsId() { return RightsId; } + public byte[] getAccountId() { return AccountId; } + public byte[] getUnknown4() { return Unknown4; } +} diff --git a/src/main/java/libKonogonka/Tools/XCI/HFS0File.java b/src/main/java/libKonogonka/Tools/XCI/HFS0File.java new file mode 100644 index 0000000..7cf9a97 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/XCI/HFS0File.java @@ -0,0 +1,44 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.XCI; + +public class HFS0File { + private String name; + private long offset; + private long size; + private long hashedRegionSize; + private boolean padding; + private byte[] SHA256Hash; + + public HFS0File(String name, long offset, long size, long hashedRegionSize, boolean padding, byte[] SHA256Hash){ + this.name = name; + this.offset = offset; + this.size = size; + this.hashedRegionSize = hashedRegionSize; + this.padding = padding; + this.SHA256Hash = SHA256Hash; + } + + public String getName() { return name; } + public long getOffset() { return offset; } + public long getSize() { return size; } + public long getHashedRegionSize() { return hashedRegionSize; } + public boolean isPadding() { return padding; } + public byte[] getSHA256Hash() { return SHA256Hash; } +} diff --git a/src/main/java/libKonogonka/Tools/XCI/HFS0Provider.java b/src/main/java/libKonogonka/Tools/XCI/HFS0Provider.java new file mode 100644 index 0000000..99fea50 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/XCI/HFS0Provider.java @@ -0,0 +1,190 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.XCI; + +import libKonogonka.Tools.ISuperProvider; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.*; + +/** + * HFS0 + * */ +public class HFS0Provider implements ISuperProvider { + + private boolean magicHFS0; + private int filesCnt; + private boolean paddingHfs0; + private int stringTableSize; + private long rawFileDataStart; + + private HFS0File[] hfs0Files; + + private File file; + + HFS0Provider(long hfsOffsetPosition, RandomAccessFile raf, File file) throws Exception{ + this.file = file; // Will be used @ getHfs0FilePipedInpStream. It's a bad implementation. + byte[] hfs0bytes = new byte[16]; + try{ + raf.seek(hfsOffsetPosition); + if (raf.read(hfs0bytes) != 16){ + throw new Exception("Read HFS0 structure failure. Can't read first 16 bytes on requested offset."); + } + } + catch (IOException ioe){ + throw new Exception("Read HFS0 structure failure. Can't read first 16 bytes on requested offset: "+ioe.getMessage()); + } + magicHFS0 = Arrays.equals(Arrays.copyOfRange(hfs0bytes, 0, 4),new byte[]{0x48, 0x46, 0x53, 0x30}); + filesCnt = getLEint(hfs0bytes, 0x4); + stringTableSize = getLEint(hfs0bytes, 8); + paddingHfs0 = Arrays.equals(Arrays.copyOfRange(hfs0bytes, 12, 16),new byte[4]); + + hfs0Files = new HFS0File[filesCnt]; + + // TODO: IF NOT EMPTY TABLE: + + long[] offsetHfs0files = new long[filesCnt]; + long[] sizeHfs0files = new long[filesCnt]; + int[] hashedRegionSizeHfs0Files = new int[filesCnt]; + boolean[] paddingHfs0Files = new boolean[filesCnt]; + byte[][] SHA256HashHfs0Files = new byte[filesCnt][]; + int[] strTableOffsets = new int[filesCnt]; + + try { + // Populate meta information regarding each file inside (?) HFS0 + byte[] metaInfoBytes = new byte[64]; + for (int i=0; i < filesCnt; i++){ + if (raf.read(metaInfoBytes) != 64) { + throw new Exception("Read HFS0 File Entry Table failure for file # "+i); + } + offsetHfs0files[i] = getLElong(metaInfoBytes, 0); + sizeHfs0files[i] = getLElong(metaInfoBytes, 8); + hashedRegionSizeHfs0Files[i] = getLEint(metaInfoBytes, 20); + paddingHfs0Files[i] = Arrays.equals(Arrays.copyOfRange(metaInfoBytes, 24, 32), new byte[8]); + SHA256HashHfs0Files[i] = Arrays.copyOfRange(metaInfoBytes, 32, 64); + + strTableOffsets[i] = getLEint(metaInfoBytes, 16); + } + // Define location of actual data for this HFS0 + rawFileDataStart = raf.getFilePointer()+stringTableSize; + if (stringTableSize <= 0) + throw new Exception("String table size of HFS0 less or equal to zero"); + byte[] stringTbl = new byte[stringTableSize]; + if (raf.read(stringTbl) != stringTableSize){ + throw new Exception("Read HFS0 String table failure. Can't read requested string table size ("+stringTableSize+")"); + } + String[] namesHfs0files = new String[filesCnt]; + // Parse string table + for (int i=0; i < filesCnt; i++){ + int j = 0; + while (stringTbl[strTableOffsets[i]+j] != (byte)0x00) + j++; + namesHfs0files[i] = new String(stringTbl, strTableOffsets[i], j, StandardCharsets.UTF_8); + } + //---------------------------------------------------------------------------------------------------------- + // Set files + for (int i=0; i < filesCnt; i++){ + hfs0Files[i] = new HFS0File( + namesHfs0files[i], + offsetHfs0files[i], + sizeHfs0files[i], + hashedRegionSizeHfs0Files[i], + paddingHfs0Files[i], + SHA256HashHfs0Files[i] + ); + } + } + catch (IOException ioe){ + throw new Exception("Read HFS0 structure failure: "+ioe.getMessage()); + } + } + + public boolean isMagicHFS0() { return magicHFS0; } + public int getFilesCnt() { return filesCnt; } + public boolean isPaddingHfs0() { return paddingHfs0; } + public int getStringTableSize() { return stringTableSize; } + @Override + public long getRawFileDataStart() { return rawFileDataStart; } + public HFS0File[] getHfs0Files() { return hfs0Files; } + @Override + public File getFile(){ return file; } + @Override + public PipedInputStream getProviderSubFilePipedInpStream(int subFileNumber) throws Exception{ + PipedOutputStream streamOut = new PipedOutputStream(); + Thread workerThread; + if (subFileNumber >= hfs0Files.length) { + throw new Exception("HFS0Provider -> getHfs0FilePipedInpStream(): Requested sub file doesn't exists"); + } + PipedInputStream streamIn = new PipedInputStream(streamOut); + + workerThread = new Thread(() -> { + System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Executing thread"); + try{ + long subFileRealPosition = rawFileDataStart + hfs0Files[subFileNumber].getOffset(); + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + if (bis.skip(subFileRealPosition) != subFileRealPosition) { + System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to skip requested offset"); + return; + } + + int readPice = 8388608; // 8mb NOTE: consider switching to 1mb 1048576 + + long readFrom = 0; + long realFileSize = hfs0Files[subFileNumber].getSize(); + + byte[] readBuf; + + while (readFrom < realFileSize){ + if (realFileSize - readFrom < readPice) + readPice = Math.toIntExact(realFileSize - readFrom); // it's safe, I guarantee + readBuf = new byte[readPice]; + if (bis.read(readBuf) != readPice) { + System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to read requested size from file."); + return; + } + streamOut.write(readBuf, 0, readPice); + readFrom += readPice; + } + bis.close(); + streamOut.close(); + } + catch (IOException ioe){ + System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Unable to provide stream"); + ioe.printStackTrace(); + } + System.out.println("HFS0Provider -> getHfs0FilePipedInpStream(): Thread died"); + }); + workerThread.start(); + return streamIn; + } + /** + * Sugar + * */ + @Override + public PipedInputStream getProviderSubFilePipedInpStream(String subFileName) throws Exception { + for (int i = 0; i < hfs0Files.length; i++){ + if (hfs0Files[i].getName().equals(subFileName)) + return getProviderSubFilePipedInpStream(i); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/libKonogonka/Tools/XCI/XCIGamecardCert.java b/src/main/java/libKonogonka/Tools/XCI/XCIGamecardCert.java new file mode 100644 index 0000000..c23f4d0 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/XCI/XCIGamecardCert.java @@ -0,0 +1,67 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.XCI; + +import java.util.Arrays; + +/** + * Gamecard Cert + * */ +public class XCIGamecardCert { + private byte[] rsa2048PKCS1sig; + private byte[] magicCert; + private byte[] unknown1; + private byte kekIndex; + private byte[] unknown2; + private byte[] deviceID; + private byte[] unknown3; + private byte[] encryptedData; + + XCIGamecardCert(byte[] certBytes) throws Exception{ + if (certBytes.length != 512) + throw new Exception("XCIGamecardCert Incorrect array size. Expected 512 bytes while received "+certBytes.length); + rsa2048PKCS1sig = Arrays.copyOfRange(certBytes, 0, 256); + magicCert = Arrays.copyOfRange(certBytes, 256, 260); + unknown1 = Arrays.copyOfRange(certBytes, 260, 264); + kekIndex = certBytes[264]; + unknown2 = Arrays.copyOfRange(certBytes, 265, 272); + deviceID = Arrays.copyOfRange(certBytes, 272, 288); + unknown3 = Arrays.copyOfRange(certBytes, 288, 298); + encryptedData = Arrays.copyOfRange(certBytes, 298, 512); + /* + RainbowHexDump.hexDumpUTF8(rsa2048PKCS1sig); + RainbowHexDump.hexDumpUTF8(magicCert); + RainbowHexDump.hexDumpUTF8(unknown1); + System.out.println(kekIndex); + RainbowHexDump.hexDumpUTF8(unknown2); + RainbowHexDump.hexDumpUTF8(deviceID); + RainbowHexDump.hexDumpUTF8(unknown3); + RainbowHexDump.hexDumpUTF8(encryptedData); + */ + } + public byte[] getRsa2048PKCS1sig() { return rsa2048PKCS1sig; } + public byte[] getMagicCert() { return magicCert; } + public boolean isMagicCertOk(){ return Arrays.equals(magicCert, new byte[]{0x48, 0x45, 0x41, 0x44}); } + public byte[] getUnknown1() { return unknown1; } + public byte getKekIndex() { return kekIndex; } + public byte[] getUnknown2() { return unknown2; } + public byte[] getDeviceID() { return deviceID; } + public byte[] getUnknown3() { return unknown3; } + public byte[] getEncryptedData() { return encryptedData; } +} diff --git a/src/main/java/libKonogonka/Tools/XCI/XCIGamecardHeader.java b/src/main/java/libKonogonka/Tools/XCI/XCIGamecardHeader.java new file mode 100644 index 0000000..1240891 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/XCI/XCIGamecardHeader.java @@ -0,0 +1,149 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.XCI; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static libKonogonka.LoperConverter.getLEint; +import static libKonogonka.LoperConverter.getLElong; +/** + * Header information + * */ +public class XCIGamecardHeader{ + private byte[] rsa2048PKCS1sig; + private boolean magicHead; + private byte[] SecureAreaStartAddr; + private boolean bkupAreaStartAddr; + private byte titleKEKIndexBoth; + private byte titleKEKIndex; + private byte KEKIndex; + private byte gcSize; + private byte gcVersion; + private byte gcFlags; + private byte[] pkgID; + private long valDataEndAddr; + private byte[] gcInfoIV; + private long hfs0partOffset; + private long hfs0headerSize; + private byte[] hfs0headerSHA256; + private byte[] hfs0initDataSHA256 ; + private int secureModeFlag; + private int titleKeyFlag; + private int keyFlag; + private byte[] normAreaEndAddr; + + XCIGamecardHeader(byte[] headerBytes) throws Exception{ + if (headerBytes.length != 400) + throw new Exception("XCIGamecardHeader Incorrect array size. Expected 400 bytes while received "+headerBytes.length); + rsa2048PKCS1sig = Arrays.copyOfRange(headerBytes, 0, 256); + magicHead = Arrays.equals(Arrays.copyOfRange(headerBytes, 256, 260), new byte[]{0x48, 0x45, 0x41, 0x44}); + SecureAreaStartAddr = Arrays.copyOfRange(headerBytes, 260, 264); + bkupAreaStartAddr = Arrays.equals(Arrays.copyOfRange(headerBytes, 264, 268), new byte[]{(byte)0xff, (byte)0xff, (byte)0xff, (byte)0xff}); + titleKEKIndexBoth = headerBytes[268]; + titleKEKIndex = (byte) ((titleKEKIndexBoth >> 4) & (byte) 0x0F); + KEKIndex = (byte) (titleKEKIndexBoth & 0x0F); + gcSize = headerBytes[269]; + gcVersion = headerBytes[270]; + gcFlags = headerBytes[271]; + pkgID = Arrays.copyOfRange(headerBytes, 272, 280); + valDataEndAddr = getLElong(headerBytes, 280); //TODO: FIX/simplify // + gcInfoIV = reverseBytes(Arrays.copyOfRange(headerBytes, 288, 304)); + hfs0partOffset = getLElong(headerBytes, 304); + hfs0headerSize = getLElong(headerBytes, 312); + hfs0headerSHA256 = Arrays.copyOfRange(headerBytes, 320, 352); + hfs0initDataSHA256 = Arrays.copyOfRange(headerBytes, 352, 384); + secureModeFlag = getLEint(headerBytes, 384); + titleKeyFlag = getLEint(headerBytes, 388); + keyFlag = getLEint(headerBytes, 392); + normAreaEndAddr = Arrays.copyOfRange(headerBytes, 396, 400); + } + + public byte[] getRsa2048PKCS1sig() { return rsa2048PKCS1sig; } + public boolean isMagicHeadOk() { return magicHead; } + public byte[] getSecureAreaStartAddr() { return SecureAreaStartAddr; } + public boolean isBkupAreaStartAddrOk() { return bkupAreaStartAddr; } + public byte getTitleKEKIndexBoth() { return titleKEKIndexBoth; } + public byte getTitleKEKIndex() { return titleKEKIndex; } + public byte getKEKIndex() { return KEKIndex; } + + public byte getGcSize() { + return gcSize; + } + public byte getGcVersion() { + return gcVersion; + } + public byte getGcFlags() { + return gcFlags; + } + public byte[] getPkgID() { + return pkgID; + } + public long getValDataEndAddr() { + return valDataEndAddr; + } + public byte[] getGcInfoIV() { + return gcInfoIV; + } + public long getHfs0partOffset() { + return hfs0partOffset; + } + public long getHfs0headerSize() { + return hfs0headerSize; + } + public byte[] getHfs0headerSHA256() { + return hfs0headerSHA256; + } + public byte[] getHfs0initDataSHA256() { + return hfs0initDataSHA256; + } + public int getSecureModeFlag() { + return secureModeFlag; + } + public boolean isSecureModeFlagOk(){ + return secureModeFlag == 1; + } + public int getTitleKeyFlag() { + return titleKeyFlag; + } + public boolean istitleKeyFlagOk(){ + return titleKeyFlag == 2; + } + public int getKeyFlag() { + return keyFlag; + } + public boolean iskeyFlagOk(){ + return keyFlag == 0; + } + public byte[] getNormAreaEndAddr() { + return normAreaEndAddr; + } + + private byte[] reverseBytes(byte[] bArr){ + Byte[] objArr = new Byte[bArr.length]; + for (int i=0;i < bArr.length; i++) + objArr[i] = bArr[i]; + List bytesList = Arrays.asList(objArr); + Collections.reverse(bytesList); + for (int i=0;i < bArr.length; i++) + bArr[i] = objArr[i]; + return bArr; + } +} \ No newline at end of file diff --git a/src/main/java/libKonogonka/Tools/XCI/XCIGamecardInfo.java b/src/main/java/libKonogonka/Tools/XCI/XCIGamecardInfo.java new file mode 100644 index 0000000..7526e05 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/XCI/XCIGamecardInfo.java @@ -0,0 +1,119 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.XCI; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.util.Arrays; + +import static libKonogonka.LoperConverter.getLEint; +import static libKonogonka.LoperConverter.getLElong; + +/** + * Gamecard Info + * */ +public class XCIGamecardInfo{ + + private long fwVersion; + private byte[] accessCtrlFlags; // 0x00A10011 for 25MHz access or 0x00A10010 for 50MHz access + private int readWaitTime1; + private int readWaitTime2; + private int writeWaitTime1; + private int writeWaitTime2; + private byte[] fwMode; + private byte[] cupVersion; + private byte[] emptyPadding1; + private byte[] updPartHash; + private byte[] cupID; + private byte[] emptyPadding2; + // todo: Add factory function instead + XCIGamecardInfo(byte[] infoBytes, byte[] IV, String XCI_HEADER_KEY) throws Exception { + if (XCI_HEADER_KEY.trim().isEmpty()) + return; + if (infoBytes.length != 112) + throw new Exception("XCIGamecardInfo Incorrect array size. Expected 112 bytes while received "+infoBytes.length); + + IvParameterSpec gciIV = new IvParameterSpec(IV); + SecretKeySpec skSpec = new SecretKeySpec(hexStrToByteArray(XCI_HEADER_KEY), "AES"); + + try { + // NOTE: CBC + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, skSpec, gciIV); + + byte[] decrypted = cipher.doFinal(infoBytes); + + fwVersion = getLElong(decrypted, 0); + accessCtrlFlags = Arrays.copyOfRange(decrypted, 8, 12); + readWaitTime1 = getLEint(decrypted, 12); + readWaitTime2 = getLEint(decrypted, 16); + writeWaitTime1 = getLEint(decrypted, 20); + writeWaitTime2 = getLEint(decrypted, 24); + fwMode = Arrays.copyOfRange(decrypted, 28, 32); + cupVersion = Arrays.copyOfRange(decrypted, 32, 36); + emptyPadding1 = Arrays.copyOfRange(decrypted, 36, 40); + updPartHash = Arrays.copyOfRange(decrypted, 40, 48); + cupID = Arrays.copyOfRange(decrypted, 48, 56); + emptyPadding2 = Arrays.copyOfRange(decrypted, 56, 112); + /* + System.out.println(fwVersion); + RainbowHexDump.hexDumpUTF8(accessCtrlFlags); + System.out.println(readWaitTime1); + System.out.println(readWaitTime2); + System.out.println(writeWaitTime1); + System.out.println(writeWaitTime2); + RainbowHexDump.hexDumpUTF8(fwMode); + RainbowHexDump.hexDumpUTF8(cupVersion); + RainbowHexDump.hexDumpUTF8(emptyPadding1); + RainbowHexDump.hexDumpUTF8(updPartHash); + RainbowHexDump.hexDumpUTF8(cupID); + RainbowHexDump.hexDumpUTF8(emptyPadding2); + */ + } catch (Exception e) { + throw new Exception("XCIGamecardInfo Decryption failed: \n "+e.getMessage()); + } + + } + private byte[] hexStrToByteArray(String s) { // thanks stackoverflow + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } + + public long getFwVersion() { return fwVersion; } + public byte[] getAccessCtrlFlags() { return accessCtrlFlags; } + public int getReadWaitTime1() { return readWaitTime1; } + public int getReadWaitTime2() { return readWaitTime2; } + public int getWriteWaitTime1() { return writeWaitTime1; } + public int getWriteWaitTime2() { return writeWaitTime2; } + + public byte[] getFwMode() { return fwMode; } + public byte[] getCupVersion() { return cupVersion; } + public boolean isEmptyPadding1() { return Arrays.equals(emptyPadding1, new byte[4]); } + public byte[] getEmptyPadding1() { return emptyPadding1; } + public byte[] getUpdPartHash() { return updPartHash; } + public byte[] getCupID() { return cupID; } + public boolean isEmptyPadding2() { return Arrays.equals(emptyPadding2, new byte[56]); } + public byte[] getEmptyPadding2() { return emptyPadding2; } +} diff --git a/src/main/java/libKonogonka/Tools/XCI/XCIProvider.java b/src/main/java/libKonogonka/Tools/XCI/XCIProvider.java new file mode 100644 index 0000000..ef06f82 --- /dev/null +++ b/src/main/java/libKonogonka/Tools/XCI/XCIProvider.java @@ -0,0 +1,118 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.Tools.XCI; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +public class XCIProvider{ + // TODO: Since LOGO partition added, we have to handle it properly. Is it works?? + + //private BufferedInputStream xciBIS; + private XCIGamecardHeader xciGamecardHeader; + private XCIGamecardInfo xciGamecardInfo; + private XCIGamecardCert xciGamecardCert; + private HFS0Provider hfs0ProviderMain, + hfs0ProviderUpdate, + hfs0ProviderNormal, + hfs0ProviderSecure, + hfs0ProviderLogo; + + public XCIProvider(File file, String XCI_HEADER_KEY) throws Exception{ // TODO: ADD FILE SIZE CHECK !!! Check xciHdrKey + RandomAccessFile raf; + + try { + //xciBIS = new BufferedInputStream(new FileInputStream(file)); + raf = new RandomAccessFile(file, "r"); + } + catch (FileNotFoundException fnfe){ + throw new Exception("XCI File not found: \n "+fnfe.getMessage()); + } + + if (file.length() < 0xf010) + throw new Exception("XCI File is too small."); + + try{ + byte[] gamecardHeaderBytes = new byte[400]; + byte[] gamecardInfoBytes = new byte[112]; + byte[] gamecardCertBytes = new byte[512]; + + // Creating GC Header class + if (raf.read(gamecardHeaderBytes) != 400) { + raf.close(); + throw new Exception("XCI Can't read Gamecard Header bytes."); + } + xciGamecardHeader = new XCIGamecardHeader(gamecardHeaderBytes); // throws exception + // Creating GC Info class + if (raf.read(gamecardInfoBytes) != 112) { + raf.close(); + throw new Exception("XCI Can't read Gamecard Header bytes."); + } + xciGamecardInfo = new XCIGamecardInfo(gamecardInfoBytes, xciGamecardHeader.getGcInfoIV(), XCI_HEADER_KEY); + // Creating GC Cerfificate class + raf.seek(0x7000); + if (raf.read(gamecardCertBytes) != 512) { + raf.close(); + throw new Exception("XCI Can't read Gamecard certificate bytes."); + } + xciGamecardCert = new XCIGamecardCert(gamecardCertBytes); + + hfs0ProviderMain = new HFS0Provider(0xf000, raf, file); + if (hfs0ProviderMain.getFilesCnt() < 3){ + raf.close(); + throw new Exception("XCI Can't read Gamecard certificate bytes."); + } + // Get all partitions from the main HFS0 file + String partition; + for (HFS0File hfs0File: hfs0ProviderMain.getHfs0Files()){ + partition = hfs0File.getName(); + if (partition.equals("update")) { + hfs0ProviderUpdate = new HFS0Provider(hfs0ProviderMain.getRawFileDataStart() + hfs0File.getOffset(), raf, file); + continue; + } + if (partition.equals("normal")) { + hfs0ProviderNormal = new HFS0Provider(hfs0ProviderMain.getRawFileDataStart() + hfs0File.getOffset(), raf, file); + continue; + } + if (partition.equals("secure")) { + hfs0ProviderSecure = new HFS0Provider(hfs0ProviderMain.getRawFileDataStart() + hfs0File.getOffset(), raf, file); + continue; + } + if (partition.equals("logo")) { + hfs0ProviderLogo = new HFS0Provider(hfs0ProviderMain.getRawFileDataStart() + hfs0File.getOffset(), raf, file); + } + } + raf.close(); + } + catch (IOException ioe){ + throw new Exception("XCI Failed file analyze for ["+file.getName()+"]\n "+ioe.getMessage()); + } + } + /* Getters */ + public XCIGamecardHeader getGCHeader(){ return this.xciGamecardHeader; } + public XCIGamecardInfo getGCInfo(){ return this.xciGamecardInfo; } + public XCIGamecardCert getGCCert(){ return this.xciGamecardCert; } + public HFS0Provider getHfs0ProviderMain() { return this.hfs0ProviderMain; } + public HFS0Provider getHfs0ProviderUpdate() { return this.hfs0ProviderUpdate; } + public HFS0Provider getHfs0ProviderNormal() { return this.hfs0ProviderNormal; } + public HFS0Provider getHfs0ProviderSecure() { return this.hfs0ProviderSecure; } + public HFS0Provider getHfs0ProviderLogo() { return this.hfs0ProviderLogo; } +} \ No newline at end of file diff --git a/src/main/java/libKonogonka/ctraes/AesCtr.java b/src/main/java/libKonogonka/ctraes/AesCtr.java new file mode 100644 index 0000000..a4f8e0b --- /dev/null +++ b/src/main/java/libKonogonka/ctraes/AesCtr.java @@ -0,0 +1,53 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.ctraes; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.Security; + +public class AesCtr { + + private static boolean BCinitialized = false; + + private void initBCProvider(){ + Security.addProvider(new BouncyCastleProvider()); + BCinitialized = true; + } + + private Cipher cipher; + private SecretKeySpec key; + + public AesCtr(byte[] keyArray) throws Exception{ + if ( ! BCinitialized) + initBCProvider(); + + key = new SecretKeySpec(keyArray, "AES"); + cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC"); + } + + public byte[] decrypt(byte[] encryptedData, byte[] IVarray) throws Exception{ + IvParameterSpec iv = new IvParameterSpec(IVarray); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + return cipher.doFinal(encryptedData); + } +} diff --git a/src/main/java/libKonogonka/ctraes/AesCtrDecryptSimple.java b/src/main/java/libKonogonka/ctraes/AesCtrDecryptSimple.java new file mode 100644 index 0000000..210067b --- /dev/null +++ b/src/main/java/libKonogonka/ctraes/AesCtrDecryptSimple.java @@ -0,0 +1,63 @@ +/* + Copyright 2019-2020 Dmitry Isaenko + + This file is part of Konogonka. + + Konogonka 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. + + Konogonka 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 Konogonka. If not, see . +*/ +package libKonogonka.ctraes; + +import libKonogonka.LoperConverter; +/** + * Simplify decryption of the CTR + */ +public class AesCtrDecryptSimple { + + private long realMediaOffset; + private byte[] IVarray; + private AesCtr aesCtr; + + public AesCtrDecryptSimple(byte[] key, byte[] sectionCTR, long realMediaOffset) throws Exception{ + this.realMediaOffset = realMediaOffset; + aesCtr = new AesCtr(key); + // IV for CTR == 16 bytes + IVarray = new byte[0x10]; + // Populate first 8 bytes taken from Header's section Block CTR + System.arraycopy(LoperConverter.flip(sectionCTR), 0x0, IVarray, 0x0, 0x8); + } + + public void skipNext(){ + realMediaOffset += 0x200; + } + + public void skipNext(long blocksNum){ + if (blocksNum > 0) + realMediaOffset += blocksNum * 0x200; + } + + public byte[] dectyptNext(byte[] enctyptedBlock) throws Exception{ + updateIV(realMediaOffset); + byte[] decryptedBlock = aesCtr.decrypt(enctyptedBlock, IVarray); + realMediaOffset += 0x200; + return decryptedBlock; + } + // Populate last 8 bytes calculated. Thanks hactool project! + private void updateIV(long offset){ + offset >>= 4; + for (int i = 0; i < 0x8; i++){ + IVarray[0x10-i-1] = (byte)(offset & 0xff); // Note: issues could be here + offset >>= 8; + } + } +} diff --git a/src/main/java/libKonogonka/exceptions/EmptySectionException.java b/src/main/java/libKonogonka/exceptions/EmptySectionException.java new file mode 100644 index 0000000..fda67fa --- /dev/null +++ b/src/main/java/libKonogonka/exceptions/EmptySectionException.java @@ -0,0 +1,7 @@ +package libKonogonka.exceptions; + +public class EmptySectionException extends NullPointerException { + public EmptySectionException(String message){ + super(message); + } +} diff --git a/src/main/java/libKonogonka/xtsaes/XTSAESBlockCipher.java b/src/main/java/libKonogonka/xtsaes/XTSAESBlockCipher.java new file mode 100644 index 0000000..efa2f96 --- /dev/null +++ b/src/main/java/libKonogonka/xtsaes/XTSAESBlockCipher.java @@ -0,0 +1,116 @@ +/* + * The MIT License + * + * Copyright 2016 Ahseya. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package libKonogonka.xtsaes; + +import net.jcip.annotations.NotThreadSafe; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.Objects; +import java.util.function.LongFunction; + +/** + * XTS-AES implemented as a BlockCipher. + * + * @author Ahseya + */ +@NotThreadSafe +public class XTSAESBlockCipher implements BlockCipher { + + private final XTSCore core; + private final int blockSize; + private final int dataUnitSize; + private long dataUnit; + private int index; + + XTSAESBlockCipher( + XTSCore core, + int blockSize, + int dataUnitSize, + long dataUnit, + int index) { + + this.core = Objects.requireNonNull(core, "core"); + this.blockSize = blockSize; + this.dataUnitSize = dataUnitSize; + this.dataUnit = dataUnit; + this.index = index; + } + + XTSAESBlockCipher(XTSCore core, LongFunction tweakValueFunction, int dataUnitSize) { + this(new XTSCore(new XTSTweak(tweakValueFunction)), core.getBlockSize(), dataUnitSize, 0, 0); + } + + public XTSAESBlockCipher(boolean isDefault, LongFunction tweakValueFunction, int dataUnitSize) { + this(new XTSCore(isDefault), tweakValueFunction, dataUnitSize); + } + + public XTSAESBlockCipher(boolean isDefault, int dataUnitSize) { + this(new XTSCore(isDefault), isDefault?XTSTweak::defaultTweakFunction:XTSTweak::nintTweakFunction, dataUnitSize); + } + + @Override + public void init(boolean forEncryption, CipherParameters params) throws IllegalArgumentException { + if (params instanceof KeyParameter) { + core.init(forEncryption, (KeyParameter) params); + return; + } + throw new IllegalArgumentException("invalid params: " + params.getClass().getName()); + } + + @Override + public String getAlgorithmName() { + return core.getAlgorithmName(); + } + + @Override + public int getBlockSize() { + return blockSize; + } + + @Override + public int processBlock(byte[] in, int inOff, byte[] out, int outOff) + throws DataLengthException, IllegalStateException { + + if (index == 0) { + core.reset(dataUnit); + } + + if ((index += blockSize) == dataUnitSize) { + dataUnit++; + index = 0; + } + + return core.processBlock(in, inOff, out, outOff); + } + + @Override + public void reset() { + index = 0; + dataUnit = 0; + } +} +// TODO consider passing initial data unit sequence number in addition to key via CipherParameters subclass diff --git a/src/main/java/libKonogonka/xtsaes/XTSAESCipher.java b/src/main/java/libKonogonka/xtsaes/XTSAESCipher.java new file mode 100644 index 0000000..be62679 --- /dev/null +++ b/src/main/java/libKonogonka/xtsaes/XTSAESCipher.java @@ -0,0 +1,117 @@ +/* + * The MIT License + * + * Copyright 2016 Ahseya. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package libKonogonka.xtsaes; + +import net.jcip.annotations.NotThreadSafe; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.Objects; +import java.util.function.LongFunction; + +/** + * XTS-AES cipher with arbitrary (non 128-bit aligned) data unit lengths. + * + * @author Ahseya + */ + +/** +* Updated for special usage by Dmitry Isaenko. +* */ +@NotThreadSafe +public class XTSAESCipher { + + private final XTSCore core; + private final int blockSize; + + XTSAESCipher(XTSCore core) { + this.core = Objects.requireNonNull(core, "core"); + blockSize = core.getBlockSize(); + } + + public XTSAESCipher(LongFunction tweakFunction) { + this(new XTSCore(new XTSTweak(tweakFunction))); + } + + public XTSAESCipher(boolean isDefault) { + this(new XTSCore(isDefault)); + } + + String getAlgorithmName() { + return core.getAlgorithmName(); + } + + int getBlockSize() { + return blockSize; + } + + public XTSAESCipher init(boolean forEncryption, KeyParameter key) throws IllegalArgumentException { + core.init(forEncryption, key); + return this; + } + + public XTSAESCipher init(boolean forEncryption, KeyParameter key1, KeyParameter key2) + throws IllegalArgumentException { + + core.init(forEncryption, key1, key2); + return this; + } + + public int processDataUnit(byte[] in, int inOff, int length, byte[] out, int outOff, long sequenceNumber) + throws DataLengthException, IllegalStateException { + + core.reset(sequenceNumber); + return process(in, inOff, length, out, outOff); + } + + int process(byte[] in, int inOff, int length, byte[] out, int outOff) + throws DataLengthException, IllegalStateException { + + if (length < blockSize) { + throw new DataLengthException("data unit size too small: " + length); + } + + if (inOff + length > in.length) { + throw new DataLengthException("input buffer too small for data unit size: " + length); + } + + if (outOff + length > out.length) { + throw new DataLengthException("output buffer too small for data unit size: " + length); + } + + int to = length % blockSize == 0 + ? length + : length - (blockSize * 2); + + int i; + for (i = 0; i < to; i += blockSize) { + core.processBlock(in, inOff + i, out, outOff + i); + } + + if (length > i) { + core.processPartial(in, inOff + i, out, outOff + i, length - i); + } + return length; + } +} diff --git a/src/main/java/libKonogonka/xtsaes/XTSCore.java b/src/main/java/libKonogonka/xtsaes/XTSCore.java new file mode 100644 index 0000000..cc409e7 --- /dev/null +++ b/src/main/java/libKonogonka/xtsaes/XTSCore.java @@ -0,0 +1,154 @@ +/* + * The MIT License + * + * Copyright 2016 Ahseya. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package libKonogonka.xtsaes; + +import net.jcip.annotations.NotThreadSafe; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.util.Arrays; +import java.util.Objects; + +/** + * XTS core functions. + * + * @author Ahseya + */ + +/** + * Updated for special usage by Dmitry Isaenko. + * */ +@NotThreadSafe +class XTSCore { + + private static final int BLOCK_SIZE = 16; + + private final BlockCipher cipher; + private final XTSTweak tweak; + private boolean forEncryption; + + XTSCore(BlockCipher cipher, XTSTweak tweak) { + this.cipher = Objects.requireNonNull(cipher, "cipher"); + this.tweak = Objects.requireNonNull(tweak, "tweak"); + } + + XTSCore(XTSTweak tweak) { + this(new AESFastEngine(), tweak); + } + + XTSCore(boolean isDefault) { + this(new XTSTweak(isDefault)); + } + + XTSCore init(boolean forEncryption, KeyParameter key) throws IllegalArgumentException { + byte[] k = ((KeyParameter) key).getKey(); + if (k.length != 32 && k.length != 64) { + throw new IllegalArgumentException("bad key length: " + k.length); + } + + byte[] key1 = Arrays.copyOfRange(k, 0, k.length / 2); + byte[] key2 = Arrays.copyOfRange(k, k.length / 2, k.length); + + return init(forEncryption, new KeyParameter(key1), new KeyParameter(key2)); + } + + XTSCore init(boolean forEncryption, KeyParameter key1, KeyParameter key2) throws IllegalArgumentException { + cipher.init(forEncryption, key1); + tweak.init(key2); + this.forEncryption = forEncryption; + return this; + } + + XTSCore reset(long tweakValue) throws DataLengthException, IllegalStateException { + tweak.reset(tweakValue); + return this; + } + + String getAlgorithmName() { + return cipher.getAlgorithmName(); + } + + int getBlockSize() { + return BLOCK_SIZE; + } + + int processBlock(byte[] in, int inOff, byte[] out, int outOff) throws DataLengthException, IllegalStateException { + byte[] tweakValue = tweak.value(); + doProcessBlock(in, inOff, out, outOff, tweakValue); + tweak.next(); + return BLOCK_SIZE; + } + + int doProcessBlock(byte[] in, int inOff, byte[] out, int outOff, byte[] tweakValue) + throws DataLengthException, IllegalStateException { + + merge(in, inOff, out, outOff, tweakValue); + cipher.processBlock(out, outOff, out, outOff); + merge(out, outOff, out, outOff, tweakValue); + return BLOCK_SIZE; + } + + void merge(byte[] in, int inOff, byte[] out, int outOff, byte[] tweak) { + for (int i = 0; i < BLOCK_SIZE; i++) { + out[i + outOff] = (byte) (in[i + inOff] ^ tweak[i]); + } + } + + int processPartial(byte[] in, int inOff, byte[] out, int outOff, int length) { + if (length <= BLOCK_SIZE) { + throw new DataLengthException("input buffer too small/ missing last two blocks: " + length); + } + + if (length >= BLOCK_SIZE * 2) { + throw new DataLengthException("input buffer too large/ non-partial final block: " + length); + } + + byte[] tweakA = tweak.value(); + byte[] tweakB = tweak.next().value(); + + return forEncryption + ? XTSCore.this.doProcessPartial(in, inOff, out, outOff, length, tweakA, tweakB) + : XTSCore.this.doProcessPartial(in, inOff, out, outOff, length, tweakB, tweakA); + } + + int doProcessPartial(byte[] in, int inOff, byte[] out, int outOff, int length, byte[] tweakA, byte[] tweakB) + throws DataLengthException, IllegalStateException { + // M-1 block + doProcessBlock(in, inOff, out, outOff, tweakA); + + // Cipher stealing + byte[] buffer = Arrays.copyOfRange(out, outOff, outOff + BLOCK_SIZE); + System.arraycopy(in, inOff + BLOCK_SIZE, buffer, 0, length - BLOCK_SIZE); + + // M block + doProcessBlock(buffer, 0, buffer, 0, tweakB); + + // Copy over final block pair + System.arraycopy(out, outOff, out, outOff + BLOCK_SIZE, length - BLOCK_SIZE); + System.arraycopy(buffer, 0, out, outOff, BLOCK_SIZE); + return length; + } +} diff --git a/src/main/java/libKonogonka/xtsaes/XTSTweak.java b/src/main/java/libKonogonka/xtsaes/XTSTweak.java new file mode 100644 index 0000000..cd2c2af --- /dev/null +++ b/src/main/java/libKonogonka/xtsaes/XTSTweak.java @@ -0,0 +1,129 @@ +/* + * The MIT License + * + * Copyright 2016 Ahseya. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package libKonogonka.xtsaes; + +import net.jcip.annotations.NotThreadSafe; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.DataLengthException; +import org.bouncycastle.crypto.engines.AESFastEngine; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.util.Pack; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.LongFunction; + +/** + * XTS tweak with pluggable tweak function. + * + * @author Ahseya + */ + +/** + * Updated for special usage by Dmitry Isaenko. + * */ +@NotThreadSafe +class XTSTweak { + static byte[] nintTweakFunction(long tweakValue) { + byte[] bs = new byte[BLOCK_SIZE]; + byte[] twk = Pack.longToBigEndian(tweakValue); + int j = BLOCK_SIZE - twk.length; + for (byte b: twk){ + bs[j++] = b; + } + return bs; + } + + static byte[] defaultTweakFunction(long tweakValue) { + byte[] bs = Pack.longToLittleEndian(tweakValue); + bs = Arrays.copyOfRange(bs, 0, BLOCK_SIZE); + return bs; + } + + private static final long FDBK = 0x87; + private static final long MSB = 0x8000000000000000L; + private static final int BLOCK_SIZE = 16; + + private final BlockCipher cipher; + private final LongFunction tweakFunction; + private final byte[] tweak; + + XTSTweak(BlockCipher cipher, LongFunction tweakFunction, byte[] tweak) { + this.cipher = Objects.requireNonNull(cipher, "cipher"); + this.tweakFunction = Objects.requireNonNull(tweakFunction, "tweakFunction"); + this.tweak = Objects.requireNonNull(tweak, "tweak"); + + if (cipher.getBlockSize() != BLOCK_SIZE) { + throw new IllegalArgumentException("bad block size: " + cipher.getBlockSize()); + } + } + + XTSTweak(BlockCipher cipher, LongFunction tweakFunction) { + this(cipher, tweakFunction, new byte[cipher.getBlockSize()]); + } + + XTSTweak(LongFunction tweakFunction) { + this(new AESFastEngine(), tweakFunction); + } + + XTSTweak(boolean isDefault) { + this(isDefault + ? XTSTweak::defaultTweakFunction + : XTSTweak::nintTweakFunction); + } + + XTSTweak init(KeyParameter key) throws IllegalArgumentException { + cipher.init(true, key); + return this; + } + + XTSTweak reset(long tweakValue) throws DataLengthException, IllegalStateException { + return reset(tweakFunction.apply(tweakValue)); + } + + XTSTweak reset(byte[] tweakBytes) throws DataLengthException, IllegalStateException { + cipher.processBlock(tweakBytes, 0, tweak, 0); + return this; + } + + byte[] value() { + return Arrays.copyOf(tweak, tweak.length); + } + + XTSTweak next() { + long lo = Pack.littleEndianToLong(tweak, 0); + long hi = Pack.littleEndianToLong(tweak, 8); + + long fdbk = (hi & MSB) == 0 + ? 0L + : FDBK; + + hi = (hi << 1) | (lo >>> 63); + lo = (lo << 1) ^ fdbk; + + Pack.longToLittleEndian(lo, tweak, 0); + Pack.longToLittleEndian(hi, tweak, 8); + return this; + } +}