├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── android.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── cpp │ ├── CMakeLists.txt │ ├── json.hpp │ ├── main.cpp │ └── zygisk.hpp │ └── java │ └── es │ └── chiteroman │ └── playintegrityfix │ ├── CustomKeyStoreSpi.java │ ├── CustomPackageInfoCreator.java │ ├── CustomProvider.java │ └── EntryPoint.java ├── build.gradle.kts ├── changelog.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── module ├── META-INF │ └── com │ │ └── google │ │ └── android │ │ ├── update-binary │ │ └── updater-script ├── action.sh ├── common_func.sh ├── customize.sh ├── module.prop ├── pif.json ├── post-fs-data.sh ├── service.sh ├── uninstall.sh └── webroot │ ├── index.html │ ├── scripts.js │ └── styles.css ├── settings.gradle.kts └── update.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: chiteroman # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | polar: # Replace with a single Polar username 14 | custom: https://www.paypal.com/paypalme/chiteroman0 # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: main 6 | paths-ignore: '**.md' 7 | pull_request: 8 | branches: main 9 | paths-ignore: '**.md' 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: macos-15 15 | 16 | steps: 17 | - name: Check out 18 | uses: actions/checkout@v4 19 | with: 20 | submodules: 'recursive' 21 | fetch-depth: 1 22 | 23 | - name: Set up JDK 24 | uses: actions/setup-java@v4 25 | with: 26 | distribution: 'temurin' 27 | java-version: 21 28 | 29 | - name: Grant execute permission for gradlew 30 | run: chmod +x gradlew 31 | 32 | - name: Build with Gradle 33 | run: ./gradlew assembleRelease 34 | 35 | - name: Upload CI module zip as artifact zip 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: PlayIntegrityFix-CI_#${{ github.run_number }} 39 | path: 'module/*' 40 | compression-level: 9 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | *.dex 12 | *.so 13 | *.zip 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "app/src/main/cpp/Dobby"] 2 | path = app/src/main/cpp/Dobby 3 | url = https://github.com/chiteroman/Dobby.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Play Integrity Fix 2 | 3 | ## WARNING 4 | Google has removed its legacy checks for Android 13 and above; now the Device verdict is the same as the old Strong verdict. 5 | You will now need a valid keybox and to use the TrickyStore module if you want to pass at least the Device verdict. 6 | 7 | If you are on Android 12 or lower, you should be safe for now. 8 | 9 | A somewhat unstable solution exists: you can use PlayIntegrityFork and enable spoofVendingSdk. This should force the use of legacy checks on modern devices; however, the Play Store sometimes fails, including crashes. 10 | 11 | I recommend that if you are not a very experienced user and absolutely need your device to be certified, install the stock ROM and lock the bootloader. 12 | 13 | Issues and discussions will be closed for a few days. 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | } 4 | 5 | android { 6 | namespace = "es.chiteroman.playintegrityfix" 7 | compileSdk = 35 8 | ndkVersion = "28.1.13356709" 9 | buildToolsVersion = "36.0.0" 10 | 11 | buildFeatures { 12 | prefab = true 13 | } 14 | 15 | packaging { 16 | jniLibs { 17 | excludes += "**/libdobby.so" 18 | } 19 | resources { 20 | excludes += "**" 21 | } 22 | } 23 | 24 | defaultConfig { 25 | applicationId = "es.chiteroman.playintegrityfix" 26 | minSdk = 26 27 | targetSdk = 35 28 | versionCode = 19100 29 | versionName = "v19.1" 30 | multiDexEnabled = false 31 | 32 | externalNativeBuild { 33 | cmake { 34 | abiFilters( 35 | "arm64-v8a", 36 | "armeabi-v7a" 37 | ) 38 | 39 | arguments( 40 | "-DCMAKE_BUILD_TYPE=Release", 41 | "-DANDROID_STL=none", 42 | "-DCMAKE_BUILD_PARALLEL_LEVEL=${Runtime.getRuntime().availableProcessors()}", 43 | "-DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON", 44 | "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" 45 | ) 46 | 47 | val commonFlags = setOf( 48 | "-fno-exceptions", 49 | "-fno-rtti", 50 | "-fvisibility=hidden", 51 | "-fvisibility-inlines-hidden", 52 | "-ffunction-sections", 53 | "-fdata-sections", 54 | "-w" 55 | ) 56 | 57 | cFlags += "-std=c23" 58 | cFlags += commonFlags 59 | 60 | cppFlags += "-std=c++26" 61 | cppFlags += commonFlags 62 | } 63 | } 64 | } 65 | 66 | buildTypes { 67 | release { 68 | isMinifyEnabled = true 69 | isShrinkResources = true 70 | multiDexEnabled = false 71 | proguardFiles( 72 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 73 | ) 74 | } 75 | } 76 | 77 | compileOptions { 78 | sourceCompatibility = JavaVersion.VERSION_21 79 | targetCompatibility = JavaVersion.VERSION_21 80 | } 81 | 82 | externalNativeBuild { 83 | cmake { 84 | path("src/main/cpp/CMakeLists.txt") 85 | version = "3.30.5+" 86 | } 87 | } 88 | } 89 | 90 | dependencies { 91 | implementation(libs.cxx) 92 | implementation(libs.hiddenapibypass) 93 | } 94 | 95 | tasks.register("updateModuleProp") { 96 | doLast { 97 | val versionName = project.android.defaultConfig.versionName 98 | val versionCode = project.android.defaultConfig.versionCode 99 | 100 | val modulePropFile = project.rootDir.resolve("module/module.prop") 101 | 102 | var content = modulePropFile.readText() 103 | 104 | content = content.replace(Regex("version=.*"), "version=$versionName") 105 | content = content.replace(Regex("versionCode=.*"), "versionCode=$versionCode") 106 | 107 | modulePropFile.writeText(content) 108 | } 109 | } 110 | 111 | tasks.register("copyFiles") { 112 | dependsOn("updateModuleProp") 113 | 114 | doLast { 115 | val moduleFolder = project.rootDir.resolve("module") 116 | val dexFile = 117 | project.layout.buildDirectory.get().asFile.resolve("intermediates/dex/release/minifyReleaseWithR8/classes.dex") 118 | val soDir = 119 | project.layout.buildDirectory.get().asFile.resolve("intermediates/stripped_native_libs/release/stripReleaseDebugSymbols/out/lib") 120 | 121 | dexFile.copyTo(moduleFolder.resolve("classes.dex"), overwrite = true) 122 | 123 | soDir.walk().filter { it.isFile && it.extension == "so" }.forEach { soFile -> 124 | val abiFolder = soFile.parentFile.name 125 | val destination = moduleFolder.resolve("zygisk/$abiFolder.so") 126 | soFile.copyTo(destination, overwrite = true) 127 | } 128 | } 129 | } 130 | 131 | tasks.register("zip") { 132 | dependsOn("copyFiles") 133 | 134 | archiveFileName.set("PlayIntegrityFix_${project.android.defaultConfig.versionName}.zip") 135 | destinationDirectory.set(project.rootDir.resolve("out")) 136 | 137 | from(project.rootDir.resolve("module")) 138 | } 139 | 140 | afterEvaluate { 141 | tasks["assembleRelease"].finalizedBy("updateModuleProp", "copyFiles", "zip") 142 | } 143 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | -dontwarn * 3 | -keep class es.chiteroman.playintegrityfix.EntryPoint {public ;} 4 | -keep class es.chiteroman.playintegrityfix.CustomKeyStoreSpi 5 | -keep class es.chiteroman.playintegrityfix.CustomProvider 6 | -keep class es.chiteroman.playintegrityfix.CustomPackageInfoCreator 7 | -keep class org.lsposed.hiddenapibypass.** { *; } 8 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.30.5) 2 | 3 | project("playintegrityfix") 4 | 5 | link_libraries(log) 6 | 7 | find_package(cxx REQUIRED CONFIG) 8 | 9 | link_libraries(cxx::cxx) 10 | 11 | add_library(${CMAKE_PROJECT_NAME} SHARED main.cpp) 12 | 13 | add_subdirectory(Dobby) 14 | 15 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE dobby_static) 16 | -------------------------------------------------------------------------------- /app/src/main/cpp/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "zygisk.hpp" 5 | #include "dobby.h" 6 | #include "json.hpp" 7 | 8 | #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "PIF", __VA_ARGS__) 9 | #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "PIF", __VA_ARGS__) 10 | 11 | #define DEX_PATH "/data/adb/modules/playintegrityfix/classes.dex" 12 | 13 | #define TS_PATH "/data/adb/modules/tricky_store" 14 | 15 | #define DEFAULT_JSON "/data/adb/modules/playintegrityfix/pif.json" 16 | #define CUSTOM_JSON_FORK "/data/adb/modules/playintegrityfix/custom.pif.json" 17 | #define CUSTOM_JSON "/data/adb/pif.json" 18 | 19 | static ssize_t xread(int fd, void *buffer, size_t count_to_read) { 20 | ssize_t total_read = 0; 21 | char *current_buf = static_cast(buffer); 22 | size_t remaining_bytes = count_to_read; 23 | 24 | while (remaining_bytes > 0) { 25 | ssize_t ret = TEMP_FAILURE_RETRY(read(fd, current_buf, remaining_bytes)); 26 | 27 | if (ret < 0) { 28 | return -1; 29 | } 30 | 31 | if (ret == 0) { 32 | break; 33 | } 34 | 35 | current_buf += ret; 36 | total_read += ret; 37 | remaining_bytes -= ret; 38 | } 39 | 40 | return total_read; 41 | } 42 | 43 | static ssize_t xwrite(int fd, const void *buffer, size_t count_to_write) { 44 | ssize_t total_written = 0; 45 | const char *current_buf = static_cast(buffer); 46 | size_t remaining_bytes = count_to_write; 47 | 48 | while (remaining_bytes > 0) { 49 | ssize_t ret = TEMP_FAILURE_RETRY(write(fd, current_buf, remaining_bytes)); 50 | 51 | if (ret < 0) { 52 | return -1; 53 | } 54 | 55 | if (ret == 0) { 56 | break; 57 | } 58 | 59 | current_buf += ret; 60 | total_written += ret; 61 | remaining_bytes -= ret; 62 | } 63 | 64 | return total_written; 65 | } 66 | 67 | static bool DEBUG = false; 68 | static std::string DEVICE_INITIAL_SDK_INT = "21", SECURITY_PATCH, BUILD_ID; 69 | 70 | typedef void (*T_Callback)(void *, const char *, const char *, uint32_t); 71 | 72 | static T_Callback o_callback = nullptr; 73 | 74 | static void modify_callback(void *cookie, const char *name, const char *value, uint32_t serial) { 75 | 76 | if (!cookie || !name || !value || !o_callback) return; 77 | 78 | const char *oldValue = value; 79 | 80 | std::string_view prop(name); 81 | 82 | if (prop == "init.svc.adbd") { 83 | value = "stopped"; 84 | } else if (prop == "sys.usb.state") { 85 | value = "mtp"; 86 | } else if (prop.ends_with("api_level")) { 87 | if (!DEVICE_INITIAL_SDK_INT.empty()) { 88 | value = DEVICE_INITIAL_SDK_INT.c_str(); 89 | } 90 | } else if (prop.ends_with(".security_patch")) { 91 | if (!SECURITY_PATCH.empty()) { 92 | value = SECURITY_PATCH.c_str(); 93 | } 94 | } else if (prop.ends_with(".build.id")) { 95 | if (!BUILD_ID.empty()) { 96 | value = BUILD_ID.c_str(); 97 | } 98 | } 99 | 100 | if (strcmp(oldValue, value) == 0) { 101 | if (DEBUG) LOGD("[%s]: %s (unchanged)", name, oldValue); 102 | } else { 103 | LOGD("[%s]: %s -> %s", name, oldValue, value); 104 | } 105 | 106 | return o_callback(cookie, name, value, serial); 107 | } 108 | 109 | static void (*o_system_property_read_callback)(prop_info *, T_Callback, void *) = nullptr; 110 | 111 | static void my_system_property_read_callback(prop_info *pi, T_Callback callback, void *cookie) { 112 | if (pi && callback && cookie) o_callback = callback; 113 | return o_system_property_read_callback(pi, modify_callback, cookie); 114 | } 115 | 116 | static bool doHook() { 117 | void *ptr = DobbySymbolResolver(nullptr, "__system_property_read_callback"); 118 | 119 | if (ptr && DobbyHook(ptr, (void *) my_system_property_read_callback, 120 | (void **) &o_system_property_read_callback) == 0) { 121 | LOGD("hook __system_property_read_callback successful at %p", ptr); 122 | return true; 123 | } 124 | 125 | LOGE("hook __system_property_read_callback failed!"); 126 | return false; 127 | } 128 | 129 | class PlayIntegrityFix : public zygisk::ModuleBase { 130 | public: 131 | void onLoad(zygisk::Api *_api, JNIEnv *_env) override { 132 | this->api = _api; 133 | this->env = _env; 134 | } 135 | 136 | void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { 137 | 138 | std::string dir, name; 139 | 140 | auto rawDir = env->GetStringUTFChars(args->app_data_dir, nullptr); 141 | 142 | if (rawDir) { 143 | dir = rawDir; 144 | env->ReleaseStringUTFChars(args->app_data_dir, rawDir); 145 | } 146 | 147 | auto rawName = env->GetStringUTFChars(args->nice_name, nullptr); 148 | 149 | if (rawName) { 150 | name = rawName; 151 | env->ReleaseStringUTFChars(args->nice_name, rawName); 152 | } 153 | 154 | bool isGms = dir.ends_with("/com.google.android.gms"); 155 | bool isGmsUnstable = isGms && name == "com.google.android.gms.unstable"; 156 | 157 | if (!isGms) { 158 | api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); 159 | return; 160 | } 161 | 162 | api->setOption(zygisk::FORCE_DENYLIST_UNMOUNT); 163 | 164 | if (!isGmsUnstable) { 165 | api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); 166 | return; 167 | } 168 | 169 | int fd = api->connectCompanion(); 170 | 171 | size_t dexSize = 0, jsonSize = 0; 172 | std::string jsonStr; 173 | 174 | xread(fd, &dexSize, sizeof(size_t)); 175 | xread(fd, &jsonSize, sizeof(size_t)); 176 | 177 | if (dexSize > 0) { 178 | dexVector.resize(dexSize); 179 | xread(fd, dexVector.data(), dexSize); 180 | } 181 | 182 | if (jsonSize > 0) { 183 | jsonStr.resize(jsonSize); 184 | jsonSize = xread(fd, jsonStr.data(), jsonSize); 185 | json = nlohmann::json::parse(jsonStr, nullptr, false, true); 186 | } 187 | 188 | bool trickyStore = false; 189 | xread(fd, &trickyStore, sizeof(bool)); 190 | 191 | bool testSignedRom = false; 192 | xread(fd, &testSignedRom, sizeof(bool)); 193 | 194 | close(fd); 195 | 196 | LOGD("Dex file size: %zu", dexSize); 197 | LOGD("Json file size: %zu", jsonSize); 198 | 199 | parseJSON(); 200 | 201 | if (trickyStore) { 202 | LOGD("TrickyStore module detected!"); 203 | spoofProvider = false; 204 | spoofProps = false; 205 | } 206 | 207 | if (testSignedRom) { 208 | LOGD("--- ROM IS SIGNED WITH TEST KEYS ---"); 209 | spoofSignature = true; 210 | } 211 | } 212 | 213 | void postAppSpecialize(const zygisk::AppSpecializeArgs *args) override { 214 | if (dexVector.empty() || json.empty()) 215 | return; 216 | 217 | UpdateBuildFields(); 218 | 219 | if (spoofProvider || spoofSignature) { 220 | injectDex(); 221 | } else { 222 | LOGD("Dex file won't be injected due spoofProvider and spoofSignature are false"); 223 | } 224 | 225 | if (spoofProps) { 226 | if (!doHook()) { 227 | dlclose(); 228 | } 229 | } else { 230 | dlclose(); 231 | } 232 | 233 | json.clear(); 234 | 235 | dexVector.clear(); 236 | dexVector.shrink_to_fit(); 237 | } 238 | 239 | void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override { 240 | api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); 241 | } 242 | 243 | private: 244 | zygisk::Api *api = nullptr; 245 | JNIEnv *env = nullptr; 246 | std::vector dexVector; 247 | nlohmann::json json; 248 | bool spoofProps = true; 249 | bool spoofProvider = true; 250 | bool spoofSignature = false; 251 | 252 | void dlclose() { 253 | LOGD("dlclose zygisk lib"); 254 | api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY); 255 | } 256 | 257 | void parseJSON() { 258 | if (json.empty()) return; 259 | 260 | if (json.contains("DEVICE_INITIAL_SDK_INT")) { 261 | if (json["DEVICE_INITIAL_SDK_INT"].is_string()) { 262 | DEVICE_INITIAL_SDK_INT = json["DEVICE_INITIAL_SDK_INT"].get(); 263 | } else if (json["DEVICE_INITIAL_SDK_INT"].is_number_integer()) { 264 | DEVICE_INITIAL_SDK_INT = std::to_string(json["DEVICE_INITIAL_SDK_INT"].get()); 265 | } else { 266 | LOGE("Couldn't parse DEVICE_INITIAL_SDK_INT value!"); 267 | } 268 | json.erase("DEVICE_INITIAL_SDK_INT"); 269 | } 270 | 271 | if (json.contains("spoofProvider") && json["spoofProvider"].is_boolean()) { 272 | spoofProvider = json["spoofProvider"].get(); 273 | json.erase("spoofProvider"); 274 | } 275 | 276 | if (json.contains("spoofProps") && json["spoofProps"].is_boolean()) { 277 | spoofProps = json["spoofProps"].get(); 278 | json.erase("spoofProps"); 279 | } 280 | 281 | if (json.contains("spoofSignature") && json["spoofSignature"].is_boolean()) { 282 | spoofSignature = json["spoofSignature"].get(); 283 | json.erase("spoofSignature"); 284 | } 285 | 286 | if (json.contains("DEBUG") && json["DEBUG"].is_boolean()) { 287 | DEBUG = json["DEBUG"].get(); 288 | json.erase("DEBUG"); 289 | } 290 | 291 | if (json.contains("FINGERPRINT") && json["FINGERPRINT"].is_string()) { 292 | std::string fingerprint = json["FINGERPRINT"].get(); 293 | 294 | std::vector vector; 295 | auto parts = fingerprint | std::views::split('/'); 296 | 297 | for (const auto &part: parts) { 298 | auto subParts = std::string(part.begin(), part.end()) | std::views::split(':'); 299 | for (const auto &subPart: subParts) { 300 | vector.emplace_back(subPart.begin(), subPart.end()); 301 | } 302 | } 303 | 304 | if (vector.size() == 8) { 305 | json["BRAND"] = vector[0]; 306 | json["PRODUCT"] = vector[1]; 307 | json["DEVICE"] = vector[2]; 308 | json["RELEASE"] = vector[3]; 309 | json["ID"] = vector[4]; 310 | json["INCREMENTAL"] = vector[5]; 311 | json["TYPE"] = vector[6]; 312 | json["TAGS"] = vector[7]; 313 | } else { 314 | LOGE("Error parsing fingerprint values!"); 315 | } 316 | } 317 | 318 | if (json.contains("SECURITY_PATCH") && json["SECURITY_PATCH"].is_string()) { 319 | SECURITY_PATCH = json["SECURITY_PATCH"].get(); 320 | } 321 | 322 | if (json.contains("ID") && json["ID"].is_string()) { 323 | BUILD_ID = json["ID"].get(); 324 | } 325 | } 326 | 327 | void injectDex() { 328 | LOGD("get system classloader"); 329 | auto clClass = env->FindClass("java/lang/ClassLoader"); 330 | auto getSystemClassLoader = env->GetStaticMethodID(clClass, "getSystemClassLoader", 331 | "()Ljava/lang/ClassLoader;"); 332 | auto systemClassLoader = env->CallStaticObjectMethod(clClass, getSystemClassLoader); 333 | 334 | if (env->ExceptionCheck()) { 335 | env->ExceptionDescribe(); 336 | env->ExceptionClear(); 337 | return; 338 | } 339 | 340 | LOGD("create class loader"); 341 | auto dexClClass = env->FindClass("dalvik/system/InMemoryDexClassLoader"); 342 | auto dexClInit = env->GetMethodID(dexClClass, "", 343 | "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V"); 344 | auto buffer = env->NewDirectByteBuffer(dexVector.data(), 345 | static_cast(dexVector.size())); 346 | auto dexCl = env->NewObject(dexClClass, dexClInit, buffer, systemClassLoader); 347 | 348 | if (env->ExceptionCheck()) { 349 | env->ExceptionDescribe(); 350 | env->ExceptionClear(); 351 | return; 352 | } 353 | 354 | LOGD("load class"); 355 | auto loadClass = env->GetMethodID(clClass, "loadClass", 356 | "(Ljava/lang/String;)Ljava/lang/Class;"); 357 | auto entryClassName = env->NewStringUTF("es.chiteroman.playintegrityfix.EntryPoint"); 358 | auto entryClassObj = env->CallObjectMethod(dexCl, loadClass, entryClassName); 359 | auto entryPointClass = (jclass) entryClassObj; 360 | 361 | if (env->ExceptionCheck()) { 362 | env->ExceptionDescribe(); 363 | env->ExceptionClear(); 364 | return; 365 | } 366 | 367 | LOGD("call init"); 368 | auto entryInit = env->GetStaticMethodID(entryPointClass, "init", "(Ljava/lang/String;ZZ)V"); 369 | auto jsonStr = env->NewStringUTF(json.dump().c_str()); 370 | env->CallStaticVoidMethod(entryPointClass, entryInit, jsonStr, spoofProvider, 371 | spoofSignature); 372 | 373 | if (env->ExceptionCheck()) { 374 | env->ExceptionDescribe(); 375 | env->ExceptionClear(); 376 | } 377 | 378 | env->DeleteLocalRef(entryClassName); 379 | env->DeleteLocalRef(entryClassObj); 380 | env->DeleteLocalRef(jsonStr); 381 | env->DeleteLocalRef(dexCl); 382 | env->DeleteLocalRef(buffer); 383 | env->DeleteLocalRef(dexClClass); 384 | env->DeleteLocalRef(clClass); 385 | 386 | LOGD("jni memory free"); 387 | } 388 | 389 | void UpdateBuildFields() { 390 | jclass buildClass = env->FindClass("android/os/Build"); 391 | jclass versionClass = env->FindClass("android/os/Build$VERSION"); 392 | 393 | for (auto &[key, val]: json.items()) { 394 | if (!val.is_string()) continue; 395 | 396 | const char *fieldName = key.c_str(); 397 | 398 | jfieldID fieldID = env->GetStaticFieldID(buildClass, fieldName, "Ljava/lang/String;"); 399 | 400 | if (env->ExceptionCheck()) { 401 | env->ExceptionClear(); 402 | 403 | fieldID = env->GetStaticFieldID(versionClass, fieldName, "Ljava/lang/String;"); 404 | 405 | if (env->ExceptionCheck()) { 406 | env->ExceptionClear(); 407 | continue; 408 | } 409 | } 410 | 411 | if (fieldID != nullptr) { 412 | std::string str = val.get(); 413 | const char *value = str.c_str(); 414 | jstring jValue = env->NewStringUTF(value); 415 | 416 | env->SetStaticObjectField(buildClass, fieldID, jValue); 417 | if (env->ExceptionCheck()) { 418 | env->ExceptionClear(); 419 | continue; 420 | } 421 | 422 | LOGD("Set '%s' to '%s'", fieldName, value); 423 | } 424 | } 425 | } 426 | }; 427 | 428 | static std::vector readFile(const char *path) { 429 | FILE *file = fopen(path, "rb"); 430 | 431 | if (!file) return {}; 432 | 433 | fseek(file, 0, SEEK_END); 434 | long size = ftell(file); 435 | fseek(file, 0, SEEK_SET); 436 | 437 | std::vector vector(size); 438 | fread(vector.data(), 1, size, file); 439 | 440 | fclose(file); 441 | 442 | return vector; 443 | } 444 | 445 | static bool checkOtaZip() { 446 | std::array buffer{}; 447 | std::string result; 448 | bool found = false; 449 | 450 | std::unique_ptr pipe( 451 | popen("unzip -l /system/etc/security/otacerts.zip", "r"), pclose); 452 | if (!pipe) return false; 453 | 454 | while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { 455 | result += buffer.data(); 456 | if (result.find("test") != std::string::npos) { 457 | found = true; 458 | break; 459 | } 460 | } 461 | 462 | return found; 463 | } 464 | 465 | static void companion(int fd) { 466 | 467 | std::vector dex, json; 468 | 469 | if (std::filesystem::exists(DEX_PATH)) { 470 | dex = readFile(DEX_PATH); 471 | } 472 | 473 | if (std::filesystem::exists(CUSTOM_JSON)) { 474 | json = readFile(CUSTOM_JSON); 475 | } else if (std::filesystem::exists(CUSTOM_JSON_FORK)) { 476 | json = readFile(CUSTOM_JSON_FORK); 477 | } else if (std::filesystem::exists(DEFAULT_JSON)) { 478 | json = readFile(DEFAULT_JSON); 479 | } 480 | 481 | size_t dexSize = dex.size(); 482 | size_t jsonSize = json.size(); 483 | 484 | xwrite(fd, &dexSize, sizeof(size_t)); 485 | xwrite(fd, &jsonSize, sizeof(size_t)); 486 | 487 | if (dexSize > 0) { 488 | xwrite(fd, dex.data(), dexSize); 489 | } 490 | 491 | if (jsonSize > 0) { 492 | xwrite(fd, json.data(), jsonSize); 493 | } 494 | 495 | std::string ts(TS_PATH); 496 | bool trickyStore = std::filesystem::exists(ts) && 497 | !std::filesystem::exists(ts + "/disable") && 498 | !std::filesystem::exists(ts + "/remove"); 499 | xwrite(fd, &trickyStore, sizeof(bool)); 500 | 501 | bool testSignedRom = checkOtaZip(); 502 | xwrite(fd, &testSignedRom, sizeof(bool)); 503 | } 504 | 505 | REGISTER_ZYGISK_MODULE(PlayIntegrityFix) 506 | 507 | REGISTER_ZYGISK_COMPANION(companion) 508 | -------------------------------------------------------------------------------- /app/src/main/cpp/zygisk.hpp: -------------------------------------------------------------------------------- 1 | /* Copyright 2022-2023 John "topjohnwu" Wu 2 | * 3 | * Permission to use, copy, modify, and/or distribute this software for any 4 | * purpose with or without fee is hereby granted. 5 | * 6 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 8 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | * PERFORMANCE OF THIS SOFTWARE. 13 | */ 14 | 15 | // This is the public API for Zygisk modules. 16 | // DO NOT MODIFY ANY CODE IN THIS HEADER. 17 | 18 | #pragma once 19 | 20 | #include 21 | 22 | #define ZYGISK_API_VERSION 2 23 | 24 | /* 25 | 26 | *************** 27 | * Introduction 28 | *************** 29 | 30 | On Android, all app processes are forked from a special daemon called "Zygote". 31 | For each new app process, zygote will fork a new process and perform "specialization". 32 | This specialization operation enforces the Android security sandbox on the newly forked 33 | process to make sure that 3rd party application code is only loaded after it is being 34 | restricted within a sandbox. 35 | 36 | On Android, there is also this special process called "system_server". This single 37 | process hosts a significant portion of system services, which controls how the 38 | Android operating system and apps interact with each other. 39 | 40 | The Zygisk framework provides a way to allow developers to build modules and run custom 41 | code before and after system_server and any app processes' specialization. 42 | This enable developers to inject code and alter the behavior of system_server and app processes. 43 | 44 | Please note that modules will only be loaded after zygote has forked the child process. 45 | THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON! 46 | 47 | ********************* 48 | * Development Guide 49 | ********************* 50 | 51 | Define a class and inherit zygisk::ModuleBase to implement the functionality of your module. 52 | Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk. 53 | 54 | Example code: 55 | 56 | static jint (*orig_logger_entry_max)(JNIEnv *env); 57 | static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); } 58 | 59 | class ExampleModule : public zygisk::ModuleBase { 60 | public: 61 | void onLoad(zygisk::Api *api, JNIEnv *env) override { 62 | this->api = api; 63 | this->env = env; 64 | } 65 | void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { 66 | JNINativeMethod methods[] = { 67 | { "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max }, 68 | }; 69 | api->hookJniNativeMethods(env, "android/util/Log", methods, 1); 70 | *(void **) &orig_logger_entry_max = methods[0].fnPtr; 71 | } 72 | private: 73 | zygisk::Api *api; 74 | JNIEnv *env; 75 | }; 76 | 77 | REGISTER_ZYGISK_MODULE(ExampleModule) 78 | 79 | ----------------------------------------------------------------------------------------- 80 | 81 | Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize, 82 | or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class 83 | never runs in a true superuser environment. 84 | 85 | If your module require access to superuser permissions, you can create and register 86 | a root companion handler function. This function runs in a separate root companion 87 | daemon process, and an Unix domain socket is provided to allow you to perform IPC between 88 | your target process and the root companion process. 89 | 90 | Example code: 91 | 92 | static void example_handler(int socket) { ... } 93 | 94 | REGISTER_ZYGISK_COMPANION(example_handler) 95 | 96 | */ 97 | 98 | namespace zygisk { 99 | 100 | struct Api; 101 | struct AppSpecializeArgs; 102 | struct ServerSpecializeArgs; 103 | 104 | class ModuleBase { 105 | public: 106 | 107 | // This method is called as soon as the module is loaded into the target process. 108 | // A Zygisk API handle will be passed as an argument. 109 | virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {} 110 | 111 | // This method is called before the app process is specialized. 112 | // At this point, the process just got forked from zygote, but no app specific specialization 113 | // is applied. This means that the process does not have any sandbox restrictions and 114 | // still runs with the same privilege of zygote. 115 | // 116 | // All the arguments that will be sent and used for app specialization is passed as a single 117 | // AppSpecializeArgs object. You can read and overwrite these arguments to change how the app 118 | // process will be specialized. 119 | // 120 | // If you need to run some operations as superuser, you can call Api::connectCompanion() to 121 | // get a socket to do IPC calls with a root companion process. 122 | // See Api::connectCompanion() for more info. 123 | virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {} 124 | 125 | // This method is called after the app process is specialized. 126 | // At this point, the process has all sandbox restrictions enabled for this application. 127 | // This means that this method runs with the same privilege of the app's own code. 128 | virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {} 129 | 130 | // This method is called before the system server process is specialized. 131 | // See preAppSpecialize(args) for more info. 132 | virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {} 133 | 134 | // This method is called after the system server process is specialized. 135 | // At this point, the process runs with the privilege of system_server. 136 | virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {} 137 | }; 138 | 139 | struct AppSpecializeArgs { 140 | // Required arguments. These arguments are guaranteed to exist on all Android versions. 141 | jint &uid; 142 | jint &gid; 143 | jintArray &gids; 144 | jint &runtime_flags; 145 | jint &mount_external; 146 | jstring &se_info; 147 | jstring &nice_name; 148 | jstring &instruction_set; 149 | jstring &app_data_dir; 150 | 151 | // Optional arguments. Please check whether the pointer is null before de-referencing 152 | jboolean *const is_child_zygote; 153 | jboolean *const is_top_app; 154 | jobjectArray *const pkg_data_info_list; 155 | jobjectArray *const whitelisted_data_info_list; 156 | jboolean *const mount_data_dirs; 157 | jboolean *const mount_storage_dirs; 158 | 159 | AppSpecializeArgs() = delete; 160 | }; 161 | 162 | struct ServerSpecializeArgs { 163 | jint &uid; 164 | jint &gid; 165 | jintArray &gids; 166 | jint &runtime_flags; 167 | jlong &permitted_capabilities; 168 | jlong &effective_capabilities; 169 | 170 | ServerSpecializeArgs() = delete; 171 | }; 172 | 173 | namespace internal { 174 | struct api_table; 175 | template void entry_impl(api_table *, JNIEnv *); 176 | } 177 | 178 | // These values are used in Api::setOption(Option) 179 | enum Option : int { 180 | // Force Magisk's denylist unmount routines to run on this process. 181 | // 182 | // Setting this option only makes sense in preAppSpecialize. 183 | // The actual unmounting happens during app process specialization. 184 | // 185 | // Set this option to force all Magisk and modules' files to be unmounted from the 186 | // mount namespace of the process, regardless of the denylist enforcement status. 187 | FORCE_DENYLIST_UNMOUNT = 0, 188 | 189 | // When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize. 190 | // Be aware that after dlclose-ing your module, all of your code will be unmapped from memory. 191 | // YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS. 192 | DLCLOSE_MODULE_LIBRARY = 1, 193 | }; 194 | 195 | // Bit masks of the return value of Api::getFlags() 196 | enum StateFlag : uint32_t { 197 | // The user has granted root access to the current process 198 | PROCESS_GRANTED_ROOT = (1u << 0), 199 | 200 | // The current process was added on the denylist 201 | PROCESS_ON_DENYLIST = (1u << 1), 202 | }; 203 | 204 | // All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded 205 | // from the specialized process afterwards. 206 | struct Api { 207 | 208 | // Connect to a root companion process and get a Unix domain socket for IPC. 209 | // 210 | // This API only works in the pre[XXX]Specialize methods due to SELinux restrictions. 211 | // 212 | // The pre[XXX]Specialize methods run with the same privilege of zygote. 213 | // If you would like to do some operations with superuser permissions, register a handler 214 | // function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func). 215 | // Another good use case for a companion process is that if you want to share some resources 216 | // across multiple processes, hold the resources in the companion process and pass it over. 217 | // 218 | // The root companion process is ABI aware; that is, when calling this method from a 32-bit 219 | // process, you will be connected to a 32-bit companion process, and vice versa for 64-bit. 220 | // 221 | // Returns a file descriptor to a socket that is connected to the socket passed to your 222 | // module's companion request handler. Returns -1 if the connection attempt failed. 223 | int connectCompanion(); 224 | 225 | // Get the file descriptor of the root folder of the current module. 226 | // 227 | // This API only works in the pre[XXX]Specialize methods. 228 | // Accessing the directory returned is only possible in the pre[XXX]Specialize methods 229 | // or in the root companion process (assuming that you sent the fd over the socket). 230 | // Both restrictions are due to SELinux and UID. 231 | // 232 | // Returns -1 if errors occurred. 233 | int getModuleDir(); 234 | 235 | // Set various options for your module. 236 | // Please note that this method accepts one single option at a time. 237 | // Check zygisk::Option for the full list of options available. 238 | void setOption(Option opt); 239 | 240 | // Get information about the current process. 241 | // Returns bitwise-or'd zygisk::StateFlag values. 242 | uint32_t getFlags(); 243 | 244 | // Hook JNI native methods for a class 245 | // 246 | // Lookup all registered JNI native methods and replace it with your own methods. 247 | // The original function pointer will be saved in each JNINativeMethod's fnPtr. 248 | // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr 249 | // will be set to nullptr. 250 | void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); 251 | 252 | // Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory. 253 | // 254 | // Parsing /proc/[PID]/maps will give you the memory map of a process. As an example: 255 | // 256 | //
257 | // 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64 258 | // (More details: https://man7.org/linux/man-pages/man5/proc.5.html) 259 | // 260 | // For ELFs loaded in memory with pathname matching `regex`, replace function `symbol` with `newFunc`. 261 | // If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`. 262 | void pltHookRegister(const char *regex, const char *symbol, void *newFunc, void **oldFunc); 263 | 264 | // For ELFs loaded in memory with pathname matching `regex`, exclude hooks registered for `symbol`. 265 | // If `symbol` is nullptr, then all symbols will be excluded. 266 | void pltHookExclude(const char *regex, const char *symbol); 267 | 268 | // Commit all the hooks that was previously registered. 269 | // Returns false if an error occurred. 270 | bool pltHookCommit(); 271 | 272 | private: 273 | internal::api_table *tbl; 274 | template friend void internal::entry_impl(internal::api_table *, JNIEnv *); 275 | }; 276 | 277 | // Register a class as a Zygisk module 278 | 279 | #define REGISTER_ZYGISK_MODULE(clazz) \ 280 | void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ 281 | zygisk::internal::entry_impl(table, env); \ 282 | } 283 | 284 | // Register a root companion request handler function for your module 285 | // 286 | // The function runs in a superuser daemon process and handles a root companion request from 287 | // your module running in a target process. The function has to accept an integer value, 288 | // which is a Unix domain socket that is connected to the target process. 289 | // See Api::connectCompanion() for more info. 290 | // 291 | // NOTE: the function can run concurrently on multiple threads. 292 | // Be aware of race conditions if you have globally shared resources. 293 | 294 | #define REGISTER_ZYGISK_COMPANION(func) \ 295 | void zygisk_companion_entry(int client) { func(client); } 296 | 297 | /********************************************************* 298 | * The following is internal ABI implementation detail. 299 | * You do not have to understand what it is doing. 300 | *********************************************************/ 301 | 302 | namespace internal { 303 | 304 | struct module_abi { 305 | long api_version; 306 | ModuleBase *impl; 307 | 308 | void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *); 309 | void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *); 310 | void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *); 311 | void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *); 312 | 313 | module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) { 314 | preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); }; 315 | postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); }; 316 | preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); }; 317 | postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); }; 318 | } 319 | }; 320 | 321 | struct api_table { 322 | // Base 323 | void *impl; 324 | bool (*registerModule)(api_table *, module_abi *); 325 | 326 | void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int); 327 | void (*pltHookRegister)(const char *, const char *, void *, void **); 328 | void (*pltHookExclude)(const char *, const char *); 329 | bool (*pltHookCommit)(); 330 | int (*connectCompanion)(void * /* impl */); 331 | void (*setOption)(void * /* impl */, Option); 332 | int (*getModuleDir)(void * /* impl */); 333 | uint32_t (*getFlags)(void * /* impl */); 334 | }; 335 | 336 | template 337 | void entry_impl(api_table *table, JNIEnv *env) { 338 | static Api api; 339 | api.tbl = table; 340 | static T module; 341 | ModuleBase *m = &module; 342 | static module_abi abi(m); 343 | if (!table->registerModule(table, &abi)) return; 344 | m->onLoad(&api, env); 345 | } 346 | 347 | } // namespace internal 348 | 349 | inline int Api::connectCompanion() { 350 | return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1; 351 | } 352 | inline int Api::getModuleDir() { 353 | return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1; 354 | } 355 | inline void Api::setOption(Option opt) { 356 | if (tbl->setOption) tbl->setOption(tbl->impl, opt); 357 | } 358 | inline uint32_t Api::getFlags() { 359 | return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0; 360 | } 361 | inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { 362 | if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods); 363 | } 364 | inline void Api::pltHookRegister(const char *regex, const char *symbol, void *newFunc, void **oldFunc) { 365 | if (tbl->pltHookRegister) tbl->pltHookRegister(regex, symbol, newFunc, oldFunc); 366 | } 367 | inline void Api::pltHookExclude(const char *regex, const char *symbol) { 368 | if (tbl->pltHookExclude) tbl->pltHookExclude(regex, symbol); 369 | } 370 | inline bool Api::pltHookCommit() { 371 | return tbl->pltHookCommit != nullptr && tbl->pltHookCommit(); 372 | } 373 | 374 | } // namespace zygisk 375 | 376 | extern "C" { 377 | 378 | [[gnu::visibility("default"), maybe_unused]] 379 | void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); 380 | 381 | [[gnu::visibility("default"), maybe_unused]] 382 | void zygisk_companion_entry(int); 383 | 384 | } // extern "C" 385 | -------------------------------------------------------------------------------- /app/src/main/java/es/chiteroman/playintegrityfix/CustomKeyStoreSpi.java: -------------------------------------------------------------------------------- 1 | package es.chiteroman.playintegrityfix; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.security.Key; 9 | import java.security.KeyStoreException; 10 | import java.security.KeyStoreSpi; 11 | import java.security.NoSuchAlgorithmException; 12 | import java.security.UnrecoverableKeyException; 13 | import java.security.cert.Certificate; 14 | import java.security.cert.CertificateException; 15 | import java.util.Date; 16 | import java.util.Enumeration; 17 | import java.util.Locale; 18 | 19 | public final class CustomKeyStoreSpi extends KeyStoreSpi { 20 | public static volatile KeyStoreSpi keyStoreSpi = null; 21 | 22 | @Override 23 | public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException { 24 | return keyStoreSpi.engineGetKey(alias, password); 25 | } 26 | 27 | @Override 28 | public Certificate[] engineGetCertificateChain(String alias) { 29 | for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace()) { 30 | if (stackTraceElement.getClassName().toLowerCase(Locale.US).contains("droidguard")) { 31 | Log.w(EntryPoint.TAG, "DroidGuard invoke engineGetCertificateChain! Throwing exception..."); 32 | throw new UnsupportedOperationException(); 33 | } 34 | } 35 | return keyStoreSpi.engineGetCertificateChain(alias); 36 | } 37 | 38 | @Override 39 | public Certificate engineGetCertificate(String alias) { 40 | return keyStoreSpi.engineGetCertificate(alias); 41 | } 42 | 43 | @Override 44 | public Date engineGetCreationDate(String alias) { 45 | return keyStoreSpi.engineGetCreationDate(alias); 46 | } 47 | 48 | @Override 49 | public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) throws KeyStoreException { 50 | keyStoreSpi.engineSetKeyEntry(alias, key, password, chain); 51 | } 52 | 53 | @Override 54 | public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException { 55 | keyStoreSpi.engineSetKeyEntry(alias, key, chain); 56 | } 57 | 58 | @Override 59 | public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException { 60 | keyStoreSpi.engineSetCertificateEntry(alias, cert); 61 | } 62 | 63 | @Override 64 | public void engineDeleteEntry(String alias) throws KeyStoreException { 65 | keyStoreSpi.engineDeleteEntry(alias); 66 | } 67 | 68 | @Override 69 | public Enumeration engineAliases() { 70 | return keyStoreSpi.engineAliases(); 71 | } 72 | 73 | @Override 74 | public boolean engineContainsAlias(String alias) { 75 | return keyStoreSpi.engineContainsAlias(alias); 76 | } 77 | 78 | @Override 79 | public int engineSize() { 80 | return keyStoreSpi.engineSize(); 81 | } 82 | 83 | @Override 84 | public boolean engineIsKeyEntry(String alias) { 85 | return keyStoreSpi.engineIsKeyEntry(alias); 86 | } 87 | 88 | @Override 89 | public boolean engineIsCertificateEntry(String alias) { 90 | return keyStoreSpi.engineIsCertificateEntry(alias); 91 | } 92 | 93 | @Override 94 | public String engineGetCertificateAlias(Certificate cert) { 95 | return keyStoreSpi.engineGetCertificateAlias(cert); 96 | } 97 | 98 | @Override 99 | public void engineStore(OutputStream stream, char[] password) throws CertificateException, IOException, NoSuchAlgorithmException { 100 | keyStoreSpi.engineStore(stream, password); 101 | } 102 | 103 | @Override 104 | public void engineLoad(InputStream stream, char[] password) throws CertificateException, IOException, NoSuchAlgorithmException { 105 | keyStoreSpi.engineLoad(stream, password); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/es/chiteroman/playintegrityfix/CustomPackageInfoCreator.java: -------------------------------------------------------------------------------- 1 | package es.chiteroman.playintegrityfix; 2 | 3 | import android.content.pm.PackageInfo; 4 | import android.content.pm.Signature; 5 | import android.os.Build; 6 | import android.os.Parcel; 7 | import android.os.Parcelable; 8 | 9 | public final class CustomPackageInfoCreator implements Parcelable.Creator { 10 | private final Parcelable.Creator originalCreator; 11 | private final Signature spoofedSignature; 12 | 13 | public CustomPackageInfoCreator(Parcelable.Creator originalCreator, Signature spoofedSignature) { 14 | this.originalCreator = originalCreator; 15 | this.spoofedSignature = spoofedSignature; 16 | } 17 | 18 | @Override 19 | public PackageInfo createFromParcel(Parcel source) { 20 | PackageInfo packageInfo = originalCreator.createFromParcel(source); 21 | if (packageInfo.packageName.equals("android")) { 22 | if (packageInfo.signatures != null && packageInfo.signatures.length > 0) { 23 | packageInfo.signatures[0] = spoofedSignature; 24 | } 25 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 26 | if (packageInfo.signingInfo != null) { 27 | Signature[] signaturesArray = packageInfo.signingInfo.getApkContentsSigners(); 28 | if (signaturesArray != null && signaturesArray.length > 0) { 29 | signaturesArray[0] = spoofedSignature; 30 | } 31 | } 32 | } 33 | } 34 | return packageInfo; 35 | } 36 | 37 | @Override 38 | public PackageInfo[] newArray(int size) { 39 | return originalCreator.newArray(size); 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/es/chiteroman/playintegrityfix/CustomProvider.java: -------------------------------------------------------------------------------- 1 | package es.chiteroman.playintegrityfix; 2 | 3 | import java.security.Provider; 4 | 5 | public final class CustomProvider extends Provider { 6 | 7 | public CustomProvider(Provider provider) { 8 | super(provider.getName(), provider.getVersion(), provider.getInfo()); 9 | putAll(provider); 10 | put("KeyStore.AndroidKeyStore", CustomKeyStoreSpi.class.getName()); 11 | } 12 | 13 | @Override 14 | public synchronized Service getService(String type, String algorithm) { 15 | EntryPoint.spoofFields(); 16 | return super.getService(type, algorithm); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/es/chiteroman/playintegrityfix/EntryPoint.java: -------------------------------------------------------------------------------- 1 | package es.chiteroman.playintegrityfix; 2 | 3 | import android.content.pm.PackageInfo; 4 | import android.content.pm.PackageManager; 5 | import android.content.pm.Signature; 6 | import android.os.Build; 7 | import android.os.Parcel; 8 | import android.os.Parcelable; 9 | import android.text.TextUtils; 10 | import android.util.Base64; 11 | import android.util.Log; 12 | 13 | import org.json.JSONObject; 14 | import org.lsposed.hiddenapibypass.HiddenApiBypass; 15 | 16 | import java.lang.reflect.Field; 17 | import java.lang.reflect.Method; 18 | import java.security.KeyStore; 19 | import java.security.KeyStoreSpi; 20 | import java.security.Provider; 21 | import java.security.Security; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.Objects; 25 | 26 | public final class EntryPoint { 27 | public static final String TAG = "PIF"; 28 | private static final Map map = new HashMap<>(); 29 | private static final String signatureData = """ 30 | MIIFyTCCA7GgAwIBAgIVALyxxl+zDS9SL68SzOr48309eAZyMA0GCSqGSIb3DQEBCwUAMHQxCzAJ 31 | BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQw 32 | EgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAg 33 | Fw0yMjExMDExODExMzVaGA8yMDUyMTEwMTE4MTEzNVowdDELMAkGA1UEBhMCVVMxEzARBgNVBAgT 34 | CkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxFDASBgNVBAoTC0dvb2dsZSBJbmMu 35 | MRAwDgYDVQQLEwdBbmRyb2lkMRAwDgYDVQQDEwdBbmRyb2lkMIICIjANBgkqhkiG9w0BAQEFAAOC 36 | Ag8AMIICCgKCAgEAsqtalIy/nctKlrhd1UVoDffFGnDf9GLi0QQhsVoJkfF16vDDydZJOycG7/kQ 37 | ziRZhFdcoMrIYZzzw0ppBjsSe1AiWMuKXwTBaEtxN99S1xsJiW4/QMI6N6kMunydWRMsbJ6aAxi1 38 | lVq0bxSwr8Sg/8u9HGVivfdG8OpUM+qjuV5gey5xttNLK3BZDrAlco8RkJZryAD40flmJZrWXJmc 39 | r2HhJJUnqG4Z3MSziEgW1u1JnnY3f/BFdgYsA54SgdUGdQP3aqzSjIpGK01/vjrXvifHazSANjvl 40 | 0AUE5i6AarMw2biEKB2ySUDp8idC5w12GpqDrhZ/QkW8yBSa87KbkMYXuRA2Gq1fYbQx3YJraw0U 41 | gZ4M3fFKpt6raxxM5j0sWHlULD7dAZMERvNESVrKG3tQ7B39WAD8QLGYc45DFEGOhKv5Fv8510h5 42 | sXK502IvGpI4FDwz2rbtAgJ0j+16db5wCSW5ThvNPhCheyciajc8dU1B5tJzZN/ksBpzne4Xf9gO 43 | LZ9ZU0+3Z5gHVvTS/YpxBFwiFpmL7dvGxew0cXGSsG5UTBlgr7i0SX0WhY4Djjo8IfPwrvvA0QaC 44 | FamdYXKqBsSHgEyXS9zgGIFPt2jWdhaS+sAa//5SXcWro0OdiKPuwEzLgj759ke1sHRnvO735dYn 45 | 5whVbzlGyLBh3L0CAwEAAaNQME4wDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUU1eXQ7NoYKjvOQlh 46 | 5V8jHQMoxA8wHwYDVR0jBBgwFoAUU1eXQ7NoYKjvOQlh5V8jHQMoxA8wDQYJKoZIhvcNAQELBQAD 47 | ggIBAHFIazRLs3itnZKllPnboSd6sHbzeJURKehx8GJPvIC+xWlwWyFO5+GHmgc3yh/SVd3Xja/k 48 | 8Ud59WEYTjyJJWTw0Jygx37rHW7VGn2HDuy/x0D+els+S8HeLD1toPFMepjIXJn7nHLhtmzTPlDW 49 | DrhiaYsls/k5Izf89xYnI4euuOY2+1gsweJqFGfbznqyqy8xLyzoZ6bvBJtgeY+G3i/9Be14HseS 50 | Na4FvI1Oze/l2gUu1IXzN6DGWR/lxEyt+TncJfBGKbjafYrfSh3zsE4N3TU7BeOL5INirOMjre/j 51 | VgB1YQG5qLVaPoz6mdn75AbBBm5a5ahApLiKqzy/hP+1rWgw8Ikb7vbUqov/bnY3IlIU6XcPJTCD 52 | b9aRZQkStvYpQd82XTyxD/T0GgRLnUj5Uv6iZlikFx1KNj0YNS2T3gyvL++J9B0Y6gAkiG0EtNpl 53 | z7Pomsv5pVdmHVdKMjqWw5/6zYzVmu5cXFtR384Ti1qwML1xkD6TC3VIv88rKIEjrkY2c+v1frh9 54 | fRJ2OmzXmML9NgHTjEiJR2Ib2iNrMKxkuTIs9oxKZgrJtJKvdU9qJJKM5PnZuNuHhGs6A/9gt9Oc 55 | cetYeQvVSqeEmQluWfcunQn9C9Vwi2BJIiVJh4IdWZf5/e2PlSSQ9CJjz2bKI17pzdxOmjQfE0JS 56 | F7Xt 57 | """; 58 | 59 | private static void spoofProvider() { 60 | try { 61 | KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 62 | Field keyStoreSpi = keyStore.getClass().getDeclaredField("keyStoreSpi"); 63 | 64 | keyStoreSpi.setAccessible(true); 65 | 66 | CustomKeyStoreSpi.keyStoreSpi = (KeyStoreSpi) keyStoreSpi.get(keyStore); 67 | 68 | keyStoreSpi.setAccessible(false); 69 | 70 | } catch (Throwable t) { 71 | Log.e(TAG, "Couldn't get keyStoreSpi field!", t); 72 | } 73 | 74 | Provider provider = Security.getProvider("AndroidKeyStore"); 75 | 76 | Provider customProvider = new CustomProvider(provider); 77 | 78 | Security.removeProvider("AndroidKeyStore"); 79 | Security.insertProviderAt(customProvider, 1); 80 | } 81 | 82 | private static void spoofSignature() { 83 | Signature spoofedSignature = new Signature(Base64.decode(signatureData, Base64.DEFAULT)); 84 | Parcelable.Creator originalCreator = PackageInfo.CREATOR; 85 | Parcelable.Creator customCreator = new CustomPackageInfoCreator(originalCreator, spoofedSignature); 86 | 87 | try { 88 | Field creatorField = findField(PackageInfo.class, "CREATOR"); 89 | creatorField.setAccessible(true); 90 | creatorField.set(null, customCreator); 91 | } catch (Exception e) { 92 | Log.e(TAG, "Couldn't replace PackageInfoCreator: " + e); 93 | } 94 | 95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 96 | HiddenApiBypass.addHiddenApiExemptions("Landroid/os/Parcel;", "Landroid/content/pm", "Landroid/app"); 97 | } 98 | 99 | try { 100 | Field cacheField = findField(PackageManager.class, "sPackageInfoCache"); 101 | cacheField.setAccessible(true); 102 | Object cache = cacheField.get(null); 103 | if (cache != null) { 104 | Method clearMethod = cache.getClass().getMethod("clear"); 105 | clearMethod.invoke(cache); 106 | } 107 | } catch (Exception e) { 108 | Log.e(TAG, "Couldn't clear PackageInfoCache: " + e); 109 | } 110 | 111 | try { 112 | Field creatorsField = findField(Parcel.class, "mCreators"); 113 | creatorsField.setAccessible(true); 114 | Map mCreators = (Map) creatorsField.get(null); 115 | if (mCreators != null) mCreators.clear(); 116 | } catch (Exception e) { 117 | Log.e(TAG, "Couldn't clear Parcel mCreators: " + e); 118 | } 119 | 120 | try { 121 | Field creatorsField = findField(Parcel.class, "sPairedCreators"); 122 | creatorsField.setAccessible(true); 123 | Map sPairedCreators = (Map) creatorsField.get(null); 124 | if (sPairedCreators != null) sPairedCreators.clear(); 125 | } catch (Exception e) { 126 | Log.e(TAG, "Couldn't clear Parcel sPairedCreators: " + e); 127 | } 128 | } 129 | 130 | private static Field findField(Class currentClass, String fieldName) throws NoSuchFieldException { 131 | while (currentClass != null && !currentClass.equals(Object.class)) { 132 | try { 133 | return currentClass.getDeclaredField(fieldName); 134 | } catch (NoSuchFieldException e) { 135 | currentClass = currentClass.getSuperclass(); 136 | } 137 | } 138 | throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy of " + Objects.requireNonNull(currentClass).getName()); 139 | } 140 | 141 | private static Field getBuildField(String name) { 142 | Field field; 143 | try { 144 | field = Build.class.getField(name); 145 | } catch (NoSuchFieldException e) { 146 | try { 147 | field = Build.VERSION.class.getField(name); 148 | } catch (NoSuchFieldException ex) { 149 | return null; 150 | } 151 | } 152 | return field; 153 | } 154 | 155 | public static void init(String json, boolean spoofProvider, boolean spoofSignature) { 156 | if (spoofProvider) { 157 | spoofProvider(); 158 | } else { 159 | Log.i(TAG, "Don't spoof Provider"); 160 | } 161 | 162 | if (spoofSignature) { 163 | spoofSignature(); 164 | } else { 165 | Log.i(TAG, "Don't spoof signature"); 166 | } 167 | 168 | if (TextUtils.isEmpty(json)) { 169 | Log.e(TAG, "Json is empty!"); 170 | return; 171 | } 172 | 173 | JSONObject jsonObject; 174 | try { 175 | jsonObject = new JSONObject(json); 176 | } catch (Throwable t) { 177 | Log.e(TAG, "init", t); 178 | return; 179 | } 180 | 181 | jsonObject.keys().forEachRemaining(key -> { 182 | Field field = getBuildField(key); 183 | if (field == null) return; 184 | try { 185 | String value = jsonObject.getString(key); 186 | if (value.isBlank()) { 187 | Log.w(TAG, "Field '" + key + "' have an empty value!"); 188 | } else { 189 | map.put(field, value); 190 | } 191 | } catch (Throwable t) { 192 | Log.e(TAG, "init", t); 193 | } 194 | }); 195 | 196 | Log.i(TAG, "Parsed " + map.size() + " fields from JSON"); 197 | 198 | spoofFields(); 199 | } 200 | 201 | public static void spoofFields() { 202 | map.forEach((field, value) -> { 203 | try { 204 | field.setAccessible(true); 205 | String oldValue = (String) field.get(null); 206 | if (value.equals(oldValue)) { 207 | field.setAccessible(false); 208 | return; 209 | } 210 | field.set(null, value); 211 | field.setAccessible(false); 212 | Log.i(TAG, "Set '" + field.getName() + "' to '" + value + "'"); 213 | } catch (Throwable t) { 214 | Log.e(TAG, "spoofFields", t); 215 | } 216 | }); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | } -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | Telegram channel: 2 | https://t.me/playintegrityfix 3 | 4 | Donations: 5 | https://www.paypal.com/paypalme/chiteroman0 6 | 7 | # v19.1 8 | 9 | - Update fingerprint. 10 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Enables namespacing of each library's R class so that its R class includes only the 19 | # resources declared in the library itself and none from the library's dependencies, 20 | # thereby reducing the size of the R class for that library 21 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.8.2" 3 | cxx = "27.0.12077973" 4 | hiddenapibypass = "6.1" 5 | 6 | [libraries] 7 | cxx = { group = "org.lsposed.libcxx", name = "libcxx", version.ref = "cxx" } 8 | hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass" } 9 | 10 | [plugins] 11 | android-application = { id = "com.android.application", version.ref = "agp" } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiteroman/PlayIntegrityFix/0ef0fa6cd43044b42fef998fd3f9224131e70139/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 17 15:25:13 CEST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /module/META-INF/com/google/android/update-binary: -------------------------------------------------------------------------------- 1 | #!/sbin/sh 2 | 3 | ################# 4 | # Initialization 5 | ################# 6 | 7 | umask 022 8 | 9 | # echo before loading util_functions 10 | ui_print() { echo "$1"; } 11 | 12 | require_new_magisk() { 13 | ui_print "*******************************" 14 | ui_print " Please install Magisk v20.4+! " 15 | ui_print "*******************************" 16 | exit 1 17 | } 18 | 19 | ######################### 20 | # Load util_functions.sh 21 | ######################### 22 | 23 | OUTFD=$2 24 | ZIPFILE=$3 25 | 26 | mount /data 2>/dev/null 27 | 28 | [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk 29 | . /data/adb/magisk/util_functions.sh 30 | [ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk 31 | 32 | install_module 33 | exit 0 -------------------------------------------------------------------------------- /module/META-INF/com/google/android/updater-script: -------------------------------------------------------------------------------- 1 | #MAGISK 2 | -------------------------------------------------------------------------------- /module/action.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PATH=/data/adb/ap/bin:/data/adb/ksu/bin:/data/adb/magisk:/data/data/com.termux/files/usr/bin:$PATH 4 | MODDIR=/data/adb/modules/playintegrityfix 5 | version=$(grep "^version=" $MODDIR/module.prop | sed 's/version=//g') 6 | FORCE_PREVIEW=1 7 | 8 | # lets try to use tmpfs for processing 9 | TEMPDIR="$MODDIR/temp" #fallback 10 | [ -w /sbin ] && TEMPDIR="/sbin/playintegrityfix" 11 | [ -w /debug_ramdisk ] && TEMPDIR="/debug_ramdisk/playintegrityfix" 12 | [ -w /dev ] && TEMPDIR="/dev/playintegrityfix" 13 | mkdir -p "$TEMPDIR" 14 | cd "$TEMPDIR" 15 | 16 | echo "[+] PlayIntegrityFix $version" 17 | echo "[+] $(basename "$0")" 18 | printf "\n\n" 19 | 20 | sleep_pause() { 21 | # APatch and KernelSU needs this 22 | # but not KSU_NEXT, MMRL 23 | if [ -z "$MMRL" ] && [ -z "$KSU_NEXT" ] && { [ "$KSU" = "true" ] || [ "$APATCH" = "true" ]; }; then 24 | sleep 5 25 | fi 26 | } 27 | 28 | download_fail() { 29 | dl_domain=$(echo "$1" | awk -F[/:] '{print $4}') 30 | echo "$1" | grep -q "\.zip$" && return 31 | # Clean up on download fail 32 | rm -rf "$TEMPDIR" 33 | ping -c 1 -W 5 "$dl_domain" > /dev/null 2>&1 || { 34 | echo "[!] Unable to connect to $dl_domain, please check your internet connection and try again" 35 | sleep_pause 36 | exit 1 37 | } 38 | conflict_module=$(ls /data/adb/modules | grep busybox) 39 | for i in $conflict_module; do 40 | echo "[!] Please remove $conflict_module and try again." 41 | done 42 | echo "[!] download failed!" 43 | echo "[x] bailing out!" 44 | sleep_pause 45 | exit 1 46 | } 47 | 48 | download() { busybox wget -T 10 --no-check-certificate -qO - "$1" > "$2" || download_fail "$1"; } 49 | if command -v curl > /dev/null 2>&1; then 50 | download() { curl --connect-timeout 10 -s "$1" > "$2" || download_fail "$1"; } 51 | fi 52 | 53 | set_random_beta() { 54 | if [ "$(echo "$MODEL_LIST" | wc -l)" -ne "$(echo "$PRODUCT_LIST" | wc -l)" ]; then 55 | echo "Error: MODEL_LIST and PRODUCT_LIST have different lengths." 56 | sleep_pause 57 | exit 1 58 | fi 59 | count=$(echo "$MODEL_LIST" | wc -l) 60 | rand_index=$(( $$ % count )) 61 | MODEL=$(echo "$MODEL_LIST" | sed -n "$((rand_index + 1))p") 62 | PRODUCT=$(echo "$PRODUCT_LIST" | sed -n "$((rand_index + 1))p") 63 | } 64 | 65 | # Get latest Pixel Beta information 66 | download https://developer.android.com/about/versions PIXEL_VERSIONS_HTML 67 | BETA_URL=$(grep -o 'https://developer.android.com/about/versions/.*[0-9]"' PIXEL_VERSIONS_HTML | sort -ru | cut -d\" -f1 | head -n1) 68 | download "$BETA_URL" PIXEL_LATEST_HTML 69 | 70 | # Handle Developer Preview vs Beta 71 | if grep -qE 'Developer Preview|tooltip>.*preview program' PIXEL_LATEST_HTML && [ "$FORCE_PREVIEW" = 0 ]; then 72 | # Use the second latest version for beta 73 | BETA_URL=$(grep -o 'https://developer.android.com/about/versions/.*[0-9]"' PIXEL_VERSIONS_HTML | sort -ru | cut -d\" -f1 | head -n2 | tail -n1) 74 | download "$BETA_URL" PIXEL_BETA_HTML 75 | else 76 | mv -f PIXEL_LATEST_HTML PIXEL_BETA_HTML 77 | fi 78 | 79 | # Get OTA information 80 | OTA_URL="https://developer.android.com$(grep -o 'href=".*download-ota.*"' PIXEL_BETA_HTML | cut -d\" -f2 | head -n1)" 81 | download "$OTA_URL" PIXEL_OTA_HTML 82 | 83 | # Extract device information 84 | MODEL_LIST="$(grep -A1 'tr id=' PIXEL_OTA_HTML | grep 'td' | sed 's;.*\(.*\);\1;')" 85 | PRODUCT_LIST="$(grep -o 'ota/.*_beta' PIXEL_OTA_HTML | cut -d\/ -f2)" 86 | OTA_LIST="$(grep 'ota/.*_beta' PIXEL_OTA_HTML | cut -d\" -f2)" 87 | 88 | # Select and configure device 89 | echo "- Selecting Pixel Beta device ..." 90 | [ -z "$PRODUCT" ] && set_random_beta 91 | echo "$MODEL ($PRODUCT)" 92 | 93 | # Get device fingerprint and security patch from OTA metadata 94 | (ulimit -f 2; download "$(echo "$OTA_LIST" | grep "$PRODUCT")" PIXEL_ZIP_METADATA) >/dev/null 2>&1 95 | FINGERPRINT="$(strings PIXEL_ZIP_METADATA | grep -am1 'post-build=' | cut -d= -f2)" 96 | SECURITY_PATCH="$(strings PIXEL_ZIP_METADATA | grep -am1 'security-patch-level=' | cut -d= -f2)" 97 | 98 | # Validate required field to prevent empty pif.json 99 | if [ -z "$FINGERPRINT" ] || [ -z "$SECURITY_PATCH" ]; then 100 | # link to download pixel rom metadata that skipped connection check due to ulimit 101 | download_fail "https://dl.google.com" 102 | fi 103 | 104 | echo "- Dumping values to pif.json ..." 105 | cat < /data/adb/pif.json 115 | echo "- new pif.json saved to /data/adb/pif.json" 116 | 117 | echo "- Cleaning up ..." 118 | rm -rf "$TEMPDIR" 119 | 120 | for i in $(busybox pidof com.google.android.gms.unstable); do 121 | echo "- Killing pid $i" 122 | kill -9 "$i" 123 | done 124 | 125 | echo "- Done!" 126 | sleep_pause 127 | -------------------------------------------------------------------------------- /module/common_func.sh: -------------------------------------------------------------------------------- 1 | RESETPROP="resetprop -n" 2 | [ -f /data/adb/magisk/util_functions.sh ] && [ "$(grep MAGISK_VER_CODE /data/adb/magisk/util_functions.sh | cut -d= -f2)" -lt 27003 ] && RESETPROP=resetprop_hexpatch 3 | 4 | # resetprop_hexpatch [-f|--force] 5 | resetprop_hexpatch() { 6 | case "$1" in 7 | -f|--force) local FORCE=1; shift;; 8 | esac 9 | 10 | local NAME="$1" 11 | local NEWVALUE="$2" 12 | local CURVALUE="$(resetprop "$NAME")" 13 | 14 | [ ! "$NEWVALUE" -o ! "$CURVALUE" ] && return 1 15 | [ "$NEWVALUE" = "$CURVALUE" -a ! "$FORCE" ] && return 2 16 | 17 | local NEWLEN=${#NEWVALUE} 18 | if [ -f /dev/__properties__ ]; then 19 | local PROPFILE=/dev/__properties__ 20 | else 21 | local PROPFILE="/dev/__properties__/$(resetprop -Z "$NAME")" 22 | fi 23 | [ ! -f "$PROPFILE" ] && return 3 24 | local NAMEOFFSET=$(echo $(strings -t d "$PROPFILE" | grep "$NAME") | cut -d ' ' -f 1) 25 | 26 | # 27 | local NEWHEX="$(printf '%02x' "$NEWLEN")$(printf "$NEWVALUE" | od -A n -t x1 -v | tr -d ' \n')$(printf "%$((92-NEWLEN))s" | sed 's/ /00/g')" 28 | 29 | printf "Patch '$NAME' to '$NEWVALUE' in '$PROPFILE' @ 0x%08x -> \n[0000??$NEWHEX]\n" $((NAMEOFFSET-96)) 30 | 31 | echo -ne "\x00\x00" \ 32 | | dd obs=1 count=2 seek=$((NAMEOFFSET-96)) conv=notrunc of="$PROPFILE" 33 | echo -ne "$(printf "$NEWHEX" | sed -e 's/.\{2\}/&\\x/g' -e 's/^/\\x/' -e 's/\\x$//')" \ 34 | | dd obs=1 count=93 seek=$((NAMEOFFSET-93)) conv=notrunc of="$PROPFILE" 35 | } 36 | 37 | # resetprop_if_diff 38 | resetprop_if_diff() { 39 | local NAME="$1" 40 | local EXPECTED="$2" 41 | local CURRENT="$(resetprop "$NAME")" 42 | 43 | [ -z "$CURRENT" ] || [ "$CURRENT" = "$EXPECTED" ] || $RESETPROP "$NAME" "$EXPECTED" 44 | } 45 | 46 | # resetprop_if_match 47 | resetprop_if_match() { 48 | local NAME="$1" 49 | local CONTAINS="$2" 50 | local VALUE="$3" 51 | 52 | [[ "$(resetprop "$NAME")" = *"$CONTAINS"* ]] && $RESETPROP "$NAME" "$VALUE" 53 | } 54 | 55 | # stub for boot-time 56 | ui_print() { return; } 57 | -------------------------------------------------------------------------------- /module/customize.sh: -------------------------------------------------------------------------------- 1 | # Don't flash in recovery! 2 | if ! $BOOTMODE; then 3 | ui_print "*********************************************************" 4 | ui_print "! Install from recovery is NOT supported" 5 | ui_print "! Recovery sucks" 6 | ui_print "! Please install from Magisk / KernelSU / APatch app" 7 | abort "*********************************************************" 8 | fi 9 | 10 | # Error on < Android 8 11 | if [ "$API" -lt 26 ]; then 12 | abort "! You can't use this module on Android < 8.0" 13 | fi 14 | 15 | check_zygisk() { 16 | local ZYGISK_MODULE="/data/adb/modules/zygisksu" 17 | local REZYGISK_MODULE="/data/adb/modules/rezygisk" 18 | local MAGISK_DIR="/data/adb/magisk" 19 | local ZYGISK_MSG="Zygisk is not enabled. Please either: 20 | - Enable Zygisk in Magisk settings 21 | - Install ZygiskNext or ReZygisk module" 22 | 23 | # Check if Zygisk module directory exists 24 | if [ -d "$ZYGISK_MODULE" ] || [ -d "$REZYGISK_MODULE" ]; then 25 | return 0 26 | fi 27 | 28 | # If Magisk is installed, check Zygisk settings 29 | if [ -d "$MAGISK_DIR" ]; then 30 | # Query Zygisk status from Magisk database 31 | local ZYGISK_STATUS 32 | ZYGISK_STATUS=$(magisk --sqlite "SELECT value FROM settings WHERE key='zygisk';") 33 | 34 | # Check if Zygisk is disabled 35 | if [ "$ZYGISK_STATUS" = "value=0" ]; then 36 | abort "$ZYGISK_MSG" 37 | fi 38 | else 39 | abort "$ZYGISK_MSG" 40 | fi 41 | } 42 | 43 | # Module requires Zygisk to work 44 | check_zygisk 45 | 46 | # safetynet-fix module is obsolete and it's incompatible with PIF 47 | SNFix="/data/adb/modules/safetynet-fix" 48 | if [ -d "$SNFix" ]; then 49 | ui_print "! safetynet-fix module is obsolete and it's incompatible with PIF, it will be removed on next reboot" 50 | ui_print "! Do not install it" 51 | touch "$SNFix"/remove 52 | fi 53 | 54 | # playcurl warn 55 | if [ -d "/data/adb/modules/playcurl" ]; then 56 | ui_print "! playcurl may overwrite fingerprint with invalid one, be careful!" 57 | fi 58 | 59 | # MagiskHidePropsConf module is obsolete in Android 8+ but it shouldn't give issues 60 | if [ -d "/data/adb/modules/MagiskHidePropsConf" ]; then 61 | ui_print "! WARNING, MagiskHidePropsConf module may cause issues with PIF." 62 | fi 63 | 64 | # Check custom fingerprint 65 | if [ -f "/data/adb/pif.json" ]; then 66 | ui_print "- Backup custom pif.json" 67 | mv -f /data/adb/pif.json /data/adb/pif.json.old 68 | fi 69 | 70 | # give exec perm to action.sh 71 | chmod +x "$MODPATH/action.sh" 72 | 73 | -------------------------------------------------------------------------------- /module/module.prop: -------------------------------------------------------------------------------- 1 | id=playintegrityfix 2 | name=Play Integrity Fix 3 | version=v19.1 4 | versionCode=19100 5 | author=chiteroman 6 | description=Universal modular fix for Play Integrity (and SafetyNet) on devices running Android 8-15 7 | updateJson=https://raw.githubusercontent.com/chiteroman/PlayIntegrityFix/main/update.json 8 | -------------------------------------------------------------------------------- /module/pif.json: -------------------------------------------------------------------------------- 1 | { 2 | "FINGERPRINT": "google/oriole_beta/oriole:16/BP22.250325.012/13467521:user/release-keys", 3 | "MANUFACTURER": "Google", 4 | "MODEL": "Pixel 6", 5 | "SECURITY_PATCH": "2025-04-05" 6 | } -------------------------------------------------------------------------------- /module/post-fs-data.sh: -------------------------------------------------------------------------------- 1 | MODPATH="${0%/*}" 2 | . "$MODPATH"/common_func.sh 3 | 4 | # Remove Play Services and Play Store from Magisk DenyList when set to Enforce in normal mode 5 | if magisk --denylist status; then 6 | magisk --denylist rm com.google.android.gms 7 | else 8 | # Check if Shamiko is installed and whitelist feature isn't enabled 9 | if [ -d "/data/adb/modules/zygisk_shamiko" ] && [ ! -f "/data/adb/shamiko/whitelist" ]; then 10 | magisk --denylist add com.google.android.gms com.google.android.gms 11 | magisk --denylist add com.google.android.gms com.google.android.gms.unstable 12 | magisk --denylist add com.android.vending 13 | fi 14 | fi 15 | 16 | # Conditional early sensitive properties 17 | 18 | # Samsung 19 | resetprop_if_diff ro.boot.warranty_bit 0 20 | resetprop_if_diff ro.vendor.boot.warranty_bit 0 21 | resetprop_if_diff ro.vendor.warranty_bit 0 22 | resetprop_if_diff ro.warranty_bit 0 23 | 24 | # Realme 25 | resetprop_if_diff ro.boot.realmebootstate green 26 | 27 | # OnePlus 28 | resetprop_if_diff ro.is_ever_orange 0 29 | 30 | # Microsoft 31 | for PROP in $(resetprop | grep -oE 'ro.*.build.tags'); do 32 | resetprop_if_diff "$PROP" release-keys 33 | done 34 | 35 | # Other 36 | for PROP in $(resetprop | grep -oE 'ro.*.build.type'); do 37 | resetprop_if_diff "$PROP" user 38 | done 39 | resetprop_if_diff ro.adb.secure 1 40 | resetprop_if_diff ro.debuggable 0 41 | resetprop_if_diff ro.force.debuggable 0 42 | resetprop_if_diff ro.secure 1 43 | 44 | # Work around AOSPA PropImitationHooks conflict when their persist props don't exist 45 | if [ -n "$(resetprop ro.aospa.version)" ]; then 46 | for PROP in persist.sys.pihooks.first_api_level persist.sys.pihooks.security_patch; do 47 | resetprop | grep -q "\[$PROP\]" || resetprop -n -p "$PROP" "" 48 | done 49 | fi 50 | 51 | # Work around supported custom ROM PixelPropsUtils conflict when spoofProvider is disabled 52 | if [ -n "$(resetprop persist.sys.pixelprops.pi)" ]; then 53 | resetprop -n -p persist.sys.pixelprops.pi false 54 | resetprop -n -p persist.sys.pixelprops.gapps false 55 | resetprop -n -p persist.sys.pixelprops.gms false 56 | fi 57 | 58 | # LeafOS "gmscompat: Dynamically spoof props for GMS" 59 | # https://review.leafos.org/c/LeafOS-Project/android_frameworks_base/+/4416 60 | # https://review.leafos.org/c/LeafOS-Project/android_frameworks_base/+/4417/5 61 | if [ -f /data/system/gms_certified_props.json ] && [ ! "$(resetprop persist.sys.spoof.gms)" = "false" ]; then 62 | resetprop persist.sys.spoof.gms false 63 | fi 64 | 65 | 66 | -------------------------------------------------------------------------------- /module/service.sh: -------------------------------------------------------------------------------- 1 | MODPATH="${0%/*}" 2 | . "$MODPATH"/common_func.sh 3 | 4 | # Conditional sensitive properties 5 | 6 | # Magisk Recovery Mode 7 | resetprop_if_match ro.boot.mode recovery unknown 8 | resetprop_if_match ro.bootmode recovery unknown 9 | resetprop_if_match vendor.boot.mode recovery unknown 10 | 11 | # SELinux 12 | resetprop_if_diff ro.boot.selinux enforcing 13 | # use toybox to protect stat access time reading 14 | if [ "$(toybox cat /sys/fs/selinux/enforce)" = "0" ]; then 15 | chmod 640 /sys/fs/selinux/enforce 16 | chmod 440 /sys/fs/selinux/policy 17 | fi 18 | 19 | # Conditional late sensitive properties 20 | 21 | until [ "$(getprop sys.boot_completed)" = "1" ]; do 22 | sleep 1 23 | done 24 | 25 | # SafetyNet/Play Integrity + OEM 26 | # avoid bootloop on some Xiaomi devices 27 | resetprop_if_diff ro.secureboot.lockstate locked 28 | # avoid breaking Realme fingerprint scanners 29 | resetprop_if_diff ro.boot.flash.locked 1 30 | resetprop_if_diff ro.boot.realme.lockstate 1 31 | # avoid breaking Oppo fingerprint scanners 32 | resetprop_if_diff ro.boot.vbmeta.device_state locked 33 | # avoid breaking OnePlus display modes/fingerprint scanners 34 | resetprop_if_diff vendor.boot.verifiedbootstate green 35 | # avoid breaking OnePlus/Oppo fingerprint scanners on OOS/ColorOS 12+ 36 | resetprop_if_diff ro.boot.verifiedbootstate green 37 | resetprop_if_diff ro.boot.veritymode enforcing 38 | resetprop_if_diff vendor.boot.vbmeta.device_state locked 39 | 40 | # Other 41 | resetprop_if_diff sys.oem_unlock_allowed 0 42 | -------------------------------------------------------------------------------- /module/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # LeafOS "gmscompat: Dynamically spoof props for GMS" 4 | # https://review.leafos.org/c/LeafOS-Project/android_frameworks_base/+/4416 5 | # https://review.leafos.org/c/LeafOS-Project/android_frameworks_base/+/4417/5 6 | if [ -f /data/system/gms_certified_props.json ]; then 7 | resetprop -p --delete persist.sys.spoof.gms 8 | fi 9 | 10 | # EOF 11 | -------------------------------------------------------------------------------- /module/webroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Play Integrity Fix 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |

15 | Play Integrity Fix 16 | 17 |

18 |
19 |
20 |
21 | Fetch pif.json 22 |
23 |
24 | Use preview fingerprint 25 | 29 |
30 |
31 |
32 |
33 | output 34 |
35 | 36 | clear 37 |
38 |
39 |
40 |
41 |
42 | 43 | -------------------------------------------------------------------------------- /module/webroot/scripts.js: -------------------------------------------------------------------------------- 1 | let shellRunning = false; 2 | let initialPinchDistance = null; 3 | let currentFontSize = 14; 4 | const MIN_FONT_SIZE = 8; 5 | const MAX_FONT_SIZE = 24; 6 | 7 | /** 8 | * Executes a shell command with KernelSU privileges 9 | * @param {string} command - The shell command to execute 10 | * @returns {Promise} A promise that resolves with stdout content 11 | * @throws {Error} If command execution fails with: 12 | * - Non-zero exit code (includes stderr in error message) 13 | */ 14 | function exec(command) { 15 | return new Promise((resolve, reject) => { 16 | const callbackFuncName = `exec_callback_${Date.now()}`; 17 | window[callbackFuncName] = (errno, stdout, stderr) => { 18 | delete window[callbackFuncName]; 19 | if (errno !== 0) { 20 | reject(new Error(`Command failed with exit code ${errno}: ${stderr}`)); 21 | return; 22 | } 23 | resolve(stdout); 24 | }; 25 | try { 26 | ksu.exec(command, "{}", callbackFuncName); 27 | } catch (error) { 28 | delete window[callbackFuncName]; 29 | reject(error); 30 | } 31 | }); 32 | } 33 | 34 | /** 35 | * Spawns shell process with ksu spawn 36 | * @param {string} command - The command to execute 37 | * @param {string[]} [args=[]] - Array of arguments to pass to the command 38 | * @returns {Object} A child process object with: 39 | * - stdout: Stream for standard output 40 | * - stderr: Stream for standard error 41 | * - stdin: Stream for standard input 42 | * - on(event, listener): Attach event listener ('exit', 'error') 43 | * - emit(event, ...args): Emit events internally 44 | */ 45 | function spawn(command, args = []) { 46 | const child = { 47 | listeners: {}, 48 | stdout: { listeners: {} }, 49 | stderr: { listeners: {} }, 50 | stdin: { listeners: {} }, 51 | on: function(event, listener) { 52 | if (!this.listeners[event]) this.listeners[event] = []; 53 | this.listeners[event].push(listener); 54 | }, 55 | emit: function(event, ...args) { 56 | if (this.listeners[event]) { 57 | this.listeners[event].forEach(listener => listener(...args)); 58 | } 59 | } 60 | }; 61 | ['stdout', 'stderr', 'stdin'].forEach(io => { 62 | child[io].on = child.on.bind(child[io]); 63 | child[io].emit = child.emit.bind(child[io]); 64 | }); 65 | const callbackName = `spawn_callback_${Date.now()}`; 66 | window[callbackName] = child; 67 | child.on("exit", () => delete window[callbackName]); 68 | try { 69 | ksu.spawn(command, JSON.stringify(args), "{}", callbackName); 70 | } catch (error) { 71 | child.emit("error", error); 72 | delete window[callbackName]; 73 | } 74 | return child; 75 | } 76 | 77 | // Apply button event listeners 78 | function applyButtonEventListeners() { 79 | const fetchButton = document.getElementById('fetch'); 80 | const previewFpToggle = document.getElementById('preview-fp-toggle-container'); 81 | const clearButton = document.querySelector('.clear-terminal'); 82 | const terminal = document.querySelector('.output-terminal-content'); 83 | 84 | fetchButton.addEventListener('click', runAction); 85 | previewFpToggle.addEventListener('click', async () => { 86 | if (shellRunning) return; 87 | shellRunning = true; 88 | try { 89 | const isChecked = document.getElementById('toggle-preview-fp').checked; 90 | await exec(`sed -i 's/^FORCE_PREVIEW=.*$/FORCE_PREVIEW=${isChecked ? 0 : 1}/' /data/adb/modules/playintegrityfix/action.sh`); 91 | appendToOutput(`[+] Switched fingerprint to ${isChecked ? 'beta' : 'preview'}`); 92 | loadPreviewFingerprintConfig(); 93 | } catch (error) { 94 | appendToOutput("[!] Failed to switch fingerprint type"); 95 | console.error('Failed to switch fingerprint type:', error); 96 | } 97 | shellRunning = false; 98 | }); 99 | 100 | clearButton.addEventListener('click', () => { 101 | terminal.innerHTML = ''; 102 | currentFontSize = 14; 103 | updateFontSize(currentFontSize); 104 | }); 105 | 106 | terminal.addEventListener('touchstart', (e) => { 107 | if (e.touches.length === 2) { 108 | e.preventDefault(); 109 | initialPinchDistance = getDistance(e.touches[0], e.touches[1]); 110 | } 111 | }, { passive: false }); 112 | terminal.addEventListener('touchmove', (e) => { 113 | if (e.touches.length === 2) { 114 | e.preventDefault(); 115 | const currentDistance = getDistance(e.touches[0], e.touches[1]); 116 | 117 | if (initialPinchDistance === null) { 118 | initialPinchDistance = currentDistance; 119 | return; 120 | } 121 | 122 | const scale = currentDistance / initialPinchDistance; 123 | const newFontSize = currentFontSize * scale; 124 | updateFontSize(newFontSize); 125 | initialPinchDistance = currentDistance; 126 | } 127 | }, { passive: false }); 128 | terminal.addEventListener('touchend', () => { 129 | initialPinchDistance = null; 130 | }); 131 | } 132 | 133 | // Function to load the version from module.prop 134 | async function loadVersionFromModuleProp() { 135 | const versionElement = document.getElementById('version-text'); 136 | try { 137 | const version = await exec("grep '^version=' /data/adb/modules/playintegrityfix/module.prop | cut -d'=' -f2"); 138 | versionElement.textContent = version.trim(); 139 | } catch (error) { 140 | appendToOutput("[!] Failed to read version from module.prop"); 141 | console.error("Failed to read version from module.prop:", error); 142 | } 143 | } 144 | 145 | // Function to load preview fingerprint config 146 | async function loadPreviewFingerprintConfig() { 147 | try { 148 | const previewFpToggle = document.getElementById('toggle-preview-fp'); 149 | const isChecked = await exec(`grep -o 'FORCE_PREVIEW=[01]' /data/adb/modules/playintegrityfix/action.sh | cut -d'=' -f2`); 150 | if (isChecked === '0') { 151 | previewFpToggle.checked = false; 152 | } else { 153 | previewFpToggle.checked = true; 154 | } 155 | } catch (error) { 156 | appendToOutput("[!] Failed to load preview fingerprint config"); 157 | console.error("Failed to load preview fingerprint config:", error); 158 | } 159 | } 160 | 161 | // Function to append element in output terminal 162 | function appendToOutput(content) { 163 | const output = document.querySelector('.output-terminal-content'); 164 | if (content.trim() === "") { 165 | const lineBreak = document.createElement('br'); 166 | output.appendChild(lineBreak); 167 | } else { 168 | const line = document.createElement('p'); 169 | line.className = 'output-content'; 170 | line.innerHTML = content.replace(/ /g, ' '); 171 | output.appendChild(line); 172 | } 173 | output.scrollTop = output.scrollHeight; 174 | } 175 | 176 | // Function to run the script and display its output 177 | function runAction() { 178 | if (shellRunning) return; 179 | shellRunning = true; 180 | const scriptOutput = spawn("sh", ["/data/adb/modules/playintegrityfix/action.sh"]); 181 | scriptOutput.stdout.on('data', (data) => appendToOutput(data)); 182 | scriptOutput.stderr.on('data', (data) => appendToOutput(data)); 183 | scriptOutput.on('exit', () => { 184 | appendToOutput(""); 185 | shellRunning = false; 186 | }); 187 | scriptOutput.on('error', () => { 188 | appendToOutput("[!] Error: Fail to execute action.sh"); 189 | appendToOutput(""); 190 | shellRunning = false; 191 | }); 192 | } 193 | 194 | /** 195 | * Simulate MD3 ripple animation 196 | * Usage: class="ripple-element" style="position: relative; overflow: hidden;" 197 | * Note: Require background-color to work properly 198 | * @return {void} 199 | */ 200 | function applyRippleEffect() { 201 | document.querySelectorAll('.ripple-element').forEach(element => { 202 | if (element.dataset.rippleListener !== "true") { 203 | element.addEventListener("pointerdown", async (event) => { 204 | // Pointer up event 205 | const handlePointerUp = () => { 206 | ripple.classList.add("end"); 207 | setTimeout(() => { 208 | ripple.classList.remove("end"); 209 | ripple.remove(); 210 | }, duration * 1000); 211 | element.removeEventListener("pointerup", handlePointerUp); 212 | element.removeEventListener("pointercancel", handlePointerUp); 213 | }; 214 | element.addEventListener("pointerup", handlePointerUp); 215 | element.addEventListener("pointercancel", handlePointerUp); 216 | 217 | const ripple = document.createElement("span"); 218 | ripple.classList.add("ripple"); 219 | 220 | // Calculate ripple size and position 221 | const rect = element.getBoundingClientRect(); 222 | const width = rect.width; 223 | const size = Math.max(rect.width, rect.height); 224 | const x = event.clientX - rect.left - size / 2; 225 | const y = event.clientY - rect.top - size / 2; 226 | 227 | // Determine animation duration 228 | let duration = 0.2 + (width / 800) * 0.4; 229 | duration = Math.min(0.8, Math.max(0.2, duration)); 230 | 231 | // Set ripple styles 232 | ripple.style.width = ripple.style.height = `${size}px`; 233 | ripple.style.left = `${x}px`; 234 | ripple.style.top = `${y}px`; 235 | ripple.style.animationDuration = `${duration}s`; 236 | ripple.style.transition = `opacity ${duration}s ease`; 237 | 238 | // Adaptive color 239 | const computedStyle = window.getComputedStyle(element); 240 | const bgColor = computedStyle.backgroundColor || "rgba(0, 0, 0, 0)"; 241 | const isDarkColor = (color) => { 242 | const rgb = color.match(/\d+/g); 243 | if (!rgb) return false; 244 | const [r, g, b] = rgb.map(Number); 245 | return (r * 0.299 + g * 0.587 + b * 0.114) < 96; // Luma formula 246 | }; 247 | ripple.style.backgroundColor = isDarkColor(bgColor) ? "rgba(255, 255, 255, 0.2)" : ""; 248 | 249 | // Append ripple 250 | element.appendChild(ripple); 251 | }); 252 | element.dataset.rippleListener = "true"; 253 | } 254 | }); 255 | } 256 | 257 | // Function to check if running in MMRL 258 | async function checkMMRL() { 259 | if (typeof ksu !== 'undefined' && ksu.mmrl) { 260 | // Set status bars theme based on device theme 261 | try { 262 | $playintegrityfix.setLightStatusBars(!window.matchMedia('(prefers-color-scheme: dark)').matches) 263 | } catch (error) { 264 | console.log("Error setting status bars theme:", error) 265 | } 266 | } 267 | } 268 | 269 | function getDistance(touch1, touch2) { 270 | return Math.hypot( 271 | touch1.clientX - touch2.clientX, 272 | touch1.clientY - touch2.clientY 273 | ); 274 | } 275 | 276 | function updateFontSize(newSize) { 277 | currentFontSize = Math.min(Math.max(newSize, MIN_FONT_SIZE), MAX_FONT_SIZE); 278 | const terminal = document.querySelector('.output-terminal-content'); 279 | terminal.style.fontSize = `${currentFontSize}px`; 280 | } 281 | 282 | document.addEventListener('DOMContentLoaded', async () => { 283 | checkMMRL(); 284 | loadVersionFromModuleProp(); 285 | loadPreviewFingerprintConfig(); 286 | applyButtonEventListeners(); 287 | applyRippleEffect(); 288 | }); -------------------------------------------------------------------------------- /module/webroot/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://mui.kernelsu.org/mmrl/insets.css'); 2 | @import url('https://mui.kernelsu.org/mmrl/colors.css'); 3 | 4 | :root { 5 | /* window inset */ 6 | --top-inset: var(--window-inset-top, 0px); 7 | --bottom-inset: var(--window-inset-bottom, 0px); 8 | 9 | /* Background colors */ 10 | --bg-primary: var(--background, #EDEDED); 11 | --bg-secondary: var(--tonalSurface, #fff); 12 | --bg-input: var(--surfaceBright, #F5F5F5); 13 | 14 | /* Text colors */ 15 | --text-constant: var(--onSurface, #000); 16 | --text-primary: var(--onSurface, #000); 17 | --text-muted: #757575; 18 | 19 | /* Border colors */ 20 | --border-color: var(--outlineVariant, #ccc); 21 | 22 | /* Button colors */ 23 | --btn-primary: var(--primary, #007bff); 24 | --btn-primary-text: var(--onPrimary, #fff); 25 | } 26 | 27 | @media (prefers-color-scheme: dark) { 28 | :root { 29 | /* Background colors */ 30 | --bg-primary: var(--background, #151515); 31 | --bg-secondary: var(--tonalSurface, #292929); 32 | --bg-input: var(--surfaceBright, #1b1b1b); 33 | 34 | /* Text colors */ 35 | --text-constant: var(--onSurface, #eee);; 36 | --text-primary: var(--onSurface, #eee); 37 | --text-muted: #C2C2C2; 38 | 39 | /* Border colors */ 40 | --border-color: var(--outlineVariant, #636363); 41 | } 42 | } 43 | 44 | body { 45 | color: var(--text-primary); 46 | background-color: var(--bg-primary); 47 | padding-top: var(--top-inset); 48 | padding-bottom: var(--bottom-inset); 49 | margin: 0; 50 | } 51 | 52 | .content { 53 | padding-bottom: 30px; 54 | display: flex; 55 | flex-direction: column; 56 | align-items: center; 57 | height: calc(100vh - var(--top-inset) - var(--bottom-inset)); 58 | box-sizing: border-box; 59 | } 60 | 61 | .header { 62 | user-select: none; 63 | } 64 | 65 | .button-box { 66 | width: calc(85vw + 30px); 67 | max-width: 800px; 68 | flex-shrink: 0; 69 | background-color: var(--bg-secondary); 70 | border: none; 71 | border-radius: 15px; 72 | box-sizing: border-box; 73 | box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2); 74 | overflow: hidden; 75 | } 76 | 77 | .toggle-list { 78 | display: flex; 79 | align-items: center; 80 | background-color: var(--bg-secondary); 81 | min-height: 25px; 82 | padding: 8px 20px; 83 | white-space: nowrap; 84 | text-align: left; 85 | border-bottom: 1px solid var(--border-color);; 86 | position: relative; 87 | overflow: hidden; 88 | } 89 | 90 | .toggle-list:last-child { 91 | border-bottom: none; 92 | margin-bottom: 0; 93 | } 94 | 95 | .toggle-text { 96 | font-size: 16px; 97 | font-weight: bold; 98 | white-space: wrap; 99 | max-width: calc(100% - 76px); 100 | user-select: none; 101 | } 102 | 103 | .toggle-switch { 104 | position: relative; 105 | display: inline-block; 106 | margin-left: auto; 107 | width: 40px; 108 | height: 25px; 109 | } 110 | 111 | .toggle-switch input { 112 | opacity: 0; 113 | width: 0; 114 | height: 0; 115 | } 116 | 117 | .slider { 118 | position: absolute; 119 | top: 0; 120 | left: 0; 121 | right: 0; 122 | bottom: 0; 123 | background-color: var(--border-color); 124 | -webkit-transition: .4s; 125 | transition: .4s; 126 | } 127 | 128 | .slider:before { 129 | position: absolute; 130 | content: ""; 131 | height: 19px; 132 | width: 19px; 133 | left: 3px; 134 | bottom: 3px; 135 | background-color: var(--text-muted); 136 | transition: .4s; 137 | } 138 | 139 | input:checked+.slider { 140 | background-color: var(--btn-primary); 141 | } 142 | 143 | input:focus+.slider { 144 | box-shadow: 0 0 1px var(--btn-primary); 145 | } 146 | 147 | input:checked+.slider:before { 148 | background-color: var(--btn-primary-text); 149 | transform: translateX(15px); 150 | } 151 | 152 | .slider.round { 153 | border-radius: 25px; 154 | } 155 | 156 | .slider.round:before { 157 | border-radius: 50%; 158 | } 159 | 160 | .output-terminal { 161 | width: calc(85vw + 30px); 162 | max-width: 800px; 163 | flex-grow: 1; 164 | background-color: var(--bg-input); 165 | box-sizing: border-box; 166 | border-radius: 15px; 167 | box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.2); 168 | margin-top: 15px; 169 | overflow: hidden; 170 | } 171 | 172 | .output-terminal-header { 173 | display: flex; 174 | padding: 5px 15px; 175 | font-size: 14px; 176 | justify-content: space-between; 177 | background-color: var(--bg-secondary); 178 | color: var(--text-muted); 179 | user-select: none; 180 | } 181 | 182 | .clear-terminal { 183 | display: flex; 184 | align-items: center; 185 | 186 | svg { 187 | fill: var(--text-muted);; 188 | } 189 | } 190 | 191 | .output-terminal-content { 192 | font-family: monospace; 193 | font-size: 14px; 194 | padding: 10px; 195 | width: calc(100% - 20px); 196 | height: calc(100% - 50px); 197 | overflow-y: auto; 198 | } 199 | 200 | .output-content { 201 | position: relative; 202 | width: 100%; 203 | padding: 0; 204 | margin: 0; 205 | word-wrap: break-word; 206 | word-break: break-all; 207 | } 208 | 209 | .ripple-element { 210 | position: relative; 211 | overflow: hidden; 212 | } 213 | 214 | .ripple { 215 | position: absolute; 216 | border-radius: 50%; 217 | transform: scale(0); 218 | opacity: 1; 219 | animation: ripple-animation ease-out forwards; 220 | pointer-events: none; 221 | background: rgba(0, 0, 0, 0.2); 222 | } 223 | 224 | .ripple.end { 225 | opacity: 0; 226 | } 227 | 228 | @keyframes ripple-animation { 229 | to { 230 | transform: scale(3); 231 | } 232 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.name = "PlayIntegrityFix" 23 | include(":app") 24 | -------------------------------------------------------------------------------- /update.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v19.1", 3 | "versionCode": 19100, 4 | "zipUrl": "https://github.com/chiteroman/PlayIntegrityFix/releases/download/v19.1/PlayIntegrityFix_v19.1.zip", 5 | "changelog": "https://raw.githubusercontent.com/chiteroman/PlayIntegrityFix/main/changelog.md" 6 | } --------------------------------------------------------------------------------