├── .gitignore ├── .idea └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ ├── app-release.apk │ └── output-metadata.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── shocker │ │ └── hideapk │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── cpp │ │ ├── CMakeLists.txt │ │ └── native-lib.cpp │ ├── java │ │ └── com │ │ │ └── shocker │ │ │ ├── hideapk │ │ │ ├── hide │ │ │ │ └── HideAPK.kt │ │ │ ├── signing │ │ │ │ ├── ApkSignerV2.java │ │ │ │ ├── ByteArrayStream.java │ │ │ │ ├── CryptoUtils.java │ │ │ │ ├── JarMap.java │ │ │ │ ├── SignApk.java │ │ │ │ └── ZipUtils.java │ │ │ └── utils │ │ │ │ ├── AXML.kt │ │ │ │ └── Keygen.kt │ │ │ └── ui │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── shocker │ └── hideapk │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | .idea -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | app随机包名demo,大部分代码来源于Magisk 2 | 3 | https://pshocker.github.io/2022/06/26/Android-%E9%9A%8F%E6%9C%BA%E5%8C%85%E5%90%8D/ -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | apply plugin: 'kotlin-android' 6 | 7 | ext { 8 | sourceCompatibility = JavaVersion.VERSION_11 9 | } 10 | 11 | android { 12 | compileSdk 32 13 | 14 | defaultConfig { 15 | applicationId "com.shocker.hideapk" 16 | minSdk 21 17 | targetSdk 32 18 | versionCode 1 19 | versionName "1.0" 20 | 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | externalNativeBuild { 23 | cmake { 24 | cppFlags '' 25 | } 26 | } 27 | } 28 | 29 | buildTypes { 30 | release { 31 | minifyEnabled false 32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 33 | } 34 | } 35 | compileOptions { 36 | targetCompatibility targetCompatibility1 37 | sourceCompatibility targetCompatibility1 38 | } 39 | kotlinOptions { 40 | jvmTarget = '1.8' 41 | } 42 | externalNativeBuild { 43 | cmake { 44 | path file('src/main/cpp/CMakeLists.txt') 45 | version '3.18.1' 46 | } 47 | } 48 | buildFeatures { 49 | viewBinding true 50 | } 51 | } 52 | 53 | dependencies { 54 | def libsuVersion = '5.0.2' 55 | // The core module is used by all other components 56 | implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" 57 | // Optional: APIs for creating root services 58 | implementation "com.github.topjohnwu.libsu:service:${libsuVersion}" 59 | // Optional: For com.topjohnwu.superuser.io classes 60 | implementation "com.github.topjohnwu.libsu:io:${libsuVersion}" 61 | 62 | implementation "org.bouncycastle:bcpkix-jdk15on:1.70" 63 | implementation "com.jakewharton.timber:timber:4.7.1" 64 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' 65 | 66 | implementation 'androidx.core:core-ktx:1.7.0' 67 | implementation 'androidx.appcompat:appcompat:1.3.0' 68 | implementation 'com.google.android.material:material:1.4.0' 69 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 70 | testImplementation 'junit:junit:4.13.2' 71 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 72 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 73 | implementation "androidx.core:core-ktx:+" 74 | } 75 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/release/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/release/app-release.apk -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.shocker.hideapk", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 1, 15 | "versionName": "1.0", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/shocker/hideapk/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.shocker.hideapk", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # For more information about using CMake with Android Studio, read the 2 | # documentation: https://d.android.com/studio/projects/add-native-code.html 3 | 4 | # Sets the minimum version of CMake required to build the native library. 5 | 6 | cmake_minimum_required(VERSION 3.18.1) 7 | 8 | # Declares and names the project. 9 | 10 | project("hideapk") 11 | 12 | # Creates and names a library, sets it as either STATIC 13 | # or SHARED, and provides the relative paths to its source code. 14 | # You can define multiple libraries, and CMake builds them for you. 15 | # Gradle automatically packages shared libraries with your APK. 16 | 17 | add_library( # Sets the name of the library. 18 | hideapk 19 | 20 | # Sets the library as a shared library. 21 | SHARED 22 | 23 | # Provides a relative path to your source file(s). 24 | native-lib.cpp) 25 | 26 | # Searches for a specified prebuilt library and stores the path as a 27 | # variable. Because CMake includes system libraries in the search path by 28 | # default, you only need to specify the name of the public NDK library 29 | # you want to add. CMake verifies that the library exists before 30 | # completing its build. 31 | 32 | find_library( # Sets the name of the path variable. 33 | log-lib 34 | 35 | # Specifies the name of the NDK library that 36 | # you want CMake to locate. 37 | log) 38 | 39 | # Specifies libraries CMake should link to your target library. You 40 | # can link multiple libraries, such as libraries you define in this 41 | # build script, prebuilt third-party libraries, or system libraries. 42 | 43 | target_link_libraries( # Specifies the target library. 44 | hideapk 45 | 46 | # Links the target library to the log library 47 | # included in the NDK. 48 | ${log-lib}) -------------------------------------------------------------------------------- /app/src/main/cpp/native-lib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | extern "C" 5 | JNIEXPORT jstring JNICALL 6 | Java_com_shocker_ui_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) { 7 | // TODO: implement stringFromJNI() 8 | std::string hello = "Hello from C++"; 9 | return env->NewStringUTF(hello.c_str()); 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/hideapk/hide/HideAPK.kt: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk.hide 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.widget.Toast 7 | import com.shocker.hideapk.BuildConfig.APPLICATION_ID 8 | import com.shocker.hideapk.signing.JarMap 9 | import com.shocker.hideapk.signing.SignApk 10 | import com.shocker.hideapk.utils.AXML 11 | import com.shocker.hideapk.utils.Keygen 12 | import com.topjohnwu.superuser.Shell 13 | import com.topjohnwu.superuser.internal.UiThreadHandler 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.withContext 16 | import timber.log.Timber 17 | import java.io.File 18 | import java.io.FileOutputStream 19 | import java.io.OutputStream 20 | import java.security.SecureRandom 21 | 22 | object HideAPK { 23 | 24 | private const val ALPHA = "abcdefghijklmnopqrstuvwxyz" 25 | private const val ALPHADOTS = "$ALPHA....." 26 | private const val ANDROID_MANIFEST = "AndroidManifest.xml" 27 | 28 | 29 | 30 | private fun genPackageName(): String { 31 | val random = SecureRandom() 32 | val len = 5 + random.nextInt(15) 33 | val builder = StringBuilder(len) 34 | var next: Char 35 | var prev = 0.toChar() 36 | for (i in 0 until len) { 37 | next = if (prev == '.' || i == 0 || i == len - 1) { 38 | ALPHA[random.nextInt(ALPHA.length)] 39 | } else { 40 | ALPHADOTS[random.nextInt(ALPHADOTS.length)] 41 | } 42 | builder.append(next) 43 | prev = next 44 | } 45 | if (!builder.contains('.')) { 46 | // Pick a random index and set it as dot 47 | val idx = random.nextInt(len - 2) 48 | builder[idx + 1] = '.' 49 | } 50 | return builder.toString() 51 | } 52 | 53 | fun patch( 54 | context: Context, 55 | apk: File, out: OutputStream, 56 | pkg: String, label: CharSequence 57 | ): Boolean { 58 | val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false 59 | val name = info.applicationInfo.nonLocalizedLabel.toString() 60 | try { 61 | JarMap.open(apk, true).use { jar -> 62 | val je = jar.getJarEntry(ANDROID_MANIFEST) 63 | val xml = AXML(jar.getRawData(je)) 64 | 65 | if (!xml.findAndPatch(APPLICATION_ID to pkg, name to label.toString())) 66 | return false 67 | 68 | // Write apk changes 69 | jar.getOutputStream(je).use { it.write(xml.bytes) } 70 | val keys = Keygen(context) 71 | SignApk.sign(keys.cert, keys.key, jar, out) 72 | return true 73 | } 74 | } catch (e: Exception) { 75 | Timber.e(e) 76 | return false 77 | } 78 | } 79 | 80 | private suspend fun patchAndHide( 81 | activity: Activity, 82 | label: String, 83 | onFailure: Runnable, 84 | path: String 85 | ): Boolean { 86 | // val stub = File(activity.cacheDir, "stub.apk") 87 | val stub = File(path) 88 | 89 | // Generate a new random package name and signature 90 | val repack = File(activity.cacheDir, "patched.apk") 91 | 92 | val pkg = genPackageName() 93 | 94 | if (!patch(activity, stub, FileOutputStream(repack), pkg, label)) 95 | return false 96 | 97 | // Install 98 | val cmd = "pm install ${repack.absolutePath}" 99 | return if(Shell.su(cmd).exec().isSuccess){ 100 | UiThreadHandler.run { Toast.makeText(activity, "随机包名安装成功,应用名:${label}", Toast.LENGTH_LONG).show() } 101 | true 102 | }else{ 103 | UiThreadHandler.run { Toast.makeText(activity, "随机包名安装失败", Toast.LENGTH_LONG).show() } 104 | false 105 | } 106 | 107 | } 108 | 109 | //label:应用的名称 110 | //path:原apk安装包路径 111 | @Suppress("DEPRECATION") 112 | suspend fun hide(activity: Activity, label: String, path: String) { 113 | val onFailure = Runnable { 114 | 115 | } 116 | val success = withContext(Dispatchers.IO) { 117 | patchAndHide(activity, label, onFailure,path) 118 | } 119 | if (!success) onFailure.run() 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/hideapk/signing/ApkSignerV2.java: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk.signing; 2 | 3 | import java.nio.BufferUnderflowException; 4 | import java.nio.ByteBuffer; 5 | import java.nio.ByteOrder; 6 | import java.security.DigestException; 7 | import java.security.InvalidAlgorithmParameterException; 8 | import java.security.InvalidKeyException; 9 | import java.security.KeyFactory; 10 | import java.security.MessageDigest; 11 | import java.security.NoSuchAlgorithmException; 12 | import java.security.PrivateKey; 13 | import java.security.PublicKey; 14 | import java.security.Signature; 15 | import java.security.SignatureException; 16 | import java.security.cert.CertificateEncodingException; 17 | import java.security.cert.X509Certificate; 18 | import java.security.spec.AlgorithmParameterSpec; 19 | import java.security.spec.InvalidKeySpecException; 20 | import java.security.spec.MGF1ParameterSpec; 21 | import java.security.spec.PSSParameterSpec; 22 | import java.security.spec.X509EncodedKeySpec; 23 | import java.util.ArrayList; 24 | import java.util.HashMap; 25 | import java.util.HashSet; 26 | import java.util.List; 27 | import java.util.Map; 28 | import java.util.Set; 29 | 30 | /** 31 | * APK Signature Scheme v2 signer. 32 | * 33 | *

APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single 34 | * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and 35 | * uncompressed contents of ZIP entries. 36 | */ 37 | public abstract class ApkSignerV2 { 38 | /* 39 | * The two main goals of APK Signature Scheme v2 are: 40 | * 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature 41 | * cover every byte of the APK being signed. 42 | * 2. Enable much faster signature and integrity verification. This is achieved by requiring 43 | * only a minimal amount of APK parsing before the signature is verified, thus completely 44 | * bypassing ZIP entry decompression and by making integrity verification parallelizable by 45 | * employing a hash tree. 46 | * 47 | * The generated signature block is wrapped into an APK Signing Block and inserted into the 48 | * original APK immediately before the start of ZIP Central Directory. This is to ensure that 49 | * JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for 50 | * extensibility. For example, a future signature scheme could insert its signatures there as 51 | * well. The contract of the APK Signing Block is that all contents outside of the block must be 52 | * protected by signatures inside the block. 53 | */ 54 | 55 | public static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101; 56 | public static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102; 57 | public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103; 58 | public static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104; 59 | public static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201; 60 | public static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202; 61 | public static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301; 62 | public static final int SIGNATURE_DSA_WITH_SHA512 = 0x0302; 63 | 64 | /** 65 | * {@code .SF} file header section attribute indicating that the APK is signed not just with 66 | * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute 67 | * facilitates v2 signature stripping detection. 68 | * 69 | *

The attribute contains a comma-separated set of signature scheme IDs. 70 | */ 71 | public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed"; 72 | public static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE = "2"; 73 | 74 | private static final int CONTENT_DIGEST_CHUNKED_SHA256 = 0; 75 | private static final int CONTENT_DIGEST_CHUNKED_SHA512 = 1; 76 | 77 | private static final int CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; 78 | 79 | private static final byte[] APK_SIGNING_BLOCK_MAGIC = 80 | new byte[] { 81 | 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, 82 | 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, 83 | }; 84 | private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; 85 | 86 | private ApkSignerV2() {} 87 | 88 | /** 89 | * Signer configuration. 90 | */ 91 | public static final class SignerConfig { 92 | /** Private key. */ 93 | public PrivateKey privateKey; 94 | 95 | /** 96 | * Certificates, with the first certificate containing the public key corresponding to 97 | * {@link #privateKey}. 98 | */ 99 | public List certificates; 100 | 101 | /** 102 | * List of signature algorithms with which to sign (see {@code SIGNATURE_...} constants). 103 | */ 104 | public List signatureAlgorithms; 105 | } 106 | 107 | /** 108 | * Signs the provided APK using APK Signature Scheme v2 and returns the signed APK as a list of 109 | * consecutive chunks. 110 | * 111 | *

NOTE: To enable APK signature verifier to detect v2 signature stripping, header sections 112 | * of META-INF/*.SF files of APK being signed must contain the 113 | * {@code X-Android-APK-Signed: true} attribute. 114 | * 115 | * @param inputApk contents of the APK to be signed. The APK starts at the current position 116 | * of the buffer and ends at the limit of the buffer. 117 | * @param signerConfigs signer configurations, one for each signer. 118 | * 119 | * @throws ApkParseException if the APK cannot be parsed. 120 | * @throws InvalidKeyException if a signing key is not suitable for this signature scheme or 121 | * cannot be used in general. 122 | * @throws SignatureException if an error occurs when computing digests of generating 123 | * signatures. 124 | */ 125 | public static ByteBuffer[] sign( 126 | ByteBuffer inputApk, 127 | List signerConfigs) 128 | throws ApkParseException, InvalidKeyException, SignatureException { 129 | // Slice/create a view in the inputApk to make sure that: 130 | // 1. inputApk is what's between position and limit of the original inputApk, and 131 | // 2. changes to position, limit, and byte order are not reflected in the original. 132 | ByteBuffer originalInputApk = inputApk; 133 | inputApk = originalInputApk.slice(); 134 | inputApk.order(ByteOrder.LITTLE_ENDIAN); 135 | 136 | // Locate ZIP End of Central Directory (EoCD), Central Directory, and check that Central 137 | // Directory is immediately followed by the ZIP End of Central Directory. 138 | int eocdOffset = ZipUtils.findZipEndOfCentralDirectoryRecord(inputApk); 139 | if (eocdOffset == -1) { 140 | throw new ApkParseException("Failed to locate ZIP End of Central Directory"); 141 | } 142 | if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(inputApk, eocdOffset)) { 143 | throw new ApkParseException("ZIP64 format not supported"); 144 | } 145 | inputApk.position(eocdOffset); 146 | long centralDirSizeLong = ZipUtils.getZipEocdCentralDirectorySizeBytes(inputApk); 147 | if (centralDirSizeLong > Integer.MAX_VALUE) { 148 | throw new ApkParseException( 149 | "ZIP Central Directory size out of range: " + centralDirSizeLong); 150 | } 151 | int centralDirSize = (int) centralDirSizeLong; 152 | long centralDirOffsetLong = ZipUtils.getZipEocdCentralDirectoryOffset(inputApk); 153 | if (centralDirOffsetLong > Integer.MAX_VALUE) { 154 | throw new ApkParseException( 155 | "ZIP Central Directory offset in file out of range: " + centralDirOffsetLong); 156 | } 157 | int centralDirOffset = (int) centralDirOffsetLong; 158 | int expectedEocdOffset = centralDirOffset + centralDirSize; 159 | if (expectedEocdOffset < centralDirOffset) { 160 | throw new ApkParseException( 161 | "ZIP Central Directory extent too large. Offset: " + centralDirOffset 162 | + ", size: " + centralDirSize); 163 | } 164 | if (eocdOffset != expectedEocdOffset) { 165 | throw new ApkParseException( 166 | "ZIP Central Directory not immeiately followed by ZIP End of" 167 | + " Central Directory. CD end: " + expectedEocdOffset 168 | + ", EoCD start: " + eocdOffset); 169 | } 170 | 171 | // Create ByteBuffers holding the contents of everything before ZIP Central Directory, 172 | // ZIP Central Directory, and ZIP End of Central Directory. 173 | inputApk.clear(); 174 | ByteBuffer beforeCentralDir = getByteBuffer(inputApk, centralDirOffset); 175 | ByteBuffer centralDir = getByteBuffer(inputApk, eocdOffset - centralDirOffset); 176 | // Create a copy of End of Central Directory because we'll need modify its contents later. 177 | byte[] eocdBytes = new byte[inputApk.remaining()]; 178 | inputApk.get(eocdBytes); 179 | ByteBuffer eocd = ByteBuffer.wrap(eocdBytes); 180 | eocd.order(inputApk.order()); 181 | 182 | // Figure which which digests to use for APK contents. 183 | Set contentDigestAlgorithms = new HashSet<>(); 184 | for (SignerConfig signerConfig : signerConfigs) { 185 | for (int signatureAlgorithm : signerConfig.signatureAlgorithms) { 186 | contentDigestAlgorithms.add( 187 | getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm)); 188 | } 189 | } 190 | 191 | // Compute digests of APK contents. 192 | Map contentDigests; // digest algorithm ID -> digest 193 | try { 194 | contentDigests = 195 | computeContentDigests( 196 | contentDigestAlgorithms, 197 | new ByteBuffer[] {beforeCentralDir, centralDir, eocd}); 198 | } catch (DigestException e) { 199 | throw new SignatureException("Failed to compute digests of APK", e); 200 | } 201 | 202 | // Sign the digests and wrap the signatures and signer info into an APK Signing Block. 203 | ByteBuffer apkSigningBlock = 204 | ByteBuffer.wrap(generateApkSigningBlock(signerConfigs, contentDigests)); 205 | 206 | // Update Central Directory Offset in End of Central Directory Record. Central Directory 207 | // follows the APK Signing Block and thus is shifted by the size of the APK Signing Block. 208 | centralDirOffset += apkSigningBlock.remaining(); 209 | eocd.clear(); 210 | ZipUtils.setZipEocdCentralDirectoryOffset(eocd, centralDirOffset); 211 | 212 | // Follow the Java NIO pattern for ByteBuffer whose contents have been consumed. 213 | originalInputApk.position(originalInputApk.limit()); 214 | 215 | // Reset positions (to 0) and limits (to capacity) in the ByteBuffers below to follow the 216 | // Java NIO pattern for ByteBuffers which are ready for their contents to be read by caller. 217 | // Contrary to the name, this does not clear the contents of these ByteBuffer. 218 | beforeCentralDir.clear(); 219 | centralDir.clear(); 220 | eocd.clear(); 221 | 222 | // Insert APK Signing Block immediately before the ZIP Central Directory. 223 | return new ByteBuffer[] { 224 | beforeCentralDir, 225 | apkSigningBlock, 226 | centralDir, 227 | eocd, 228 | }; 229 | } 230 | 231 | private static Map computeContentDigests( 232 | Set digestAlgorithms, 233 | ByteBuffer[] contents) throws DigestException { 234 | // For each digest algorithm the result is computed as follows: 235 | // 1. Each segment of contents is split into consecutive chunks of 1 MB in size. 236 | // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB. 237 | // No chunks are produced for empty (zero length) segments. 238 | // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's 239 | // length in bytes (uint32 little-endian) and the chunk's contents. 240 | // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of 241 | // chunks (uint32 little-endian) and the concatenation of digests of chunks of all 242 | // segments in-order. 243 | 244 | int chunkCount = 0; 245 | for (ByteBuffer input : contents) { 246 | chunkCount += getChunkCount(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); 247 | } 248 | 249 | final Map digestsOfChunks = new HashMap<>(digestAlgorithms.size()); 250 | for (int digestAlgorithm : digestAlgorithms) { 251 | int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); 252 | byte[] concatenationOfChunkCountAndChunkDigests = 253 | new byte[5 + chunkCount * digestOutputSizeBytes]; 254 | concatenationOfChunkCountAndChunkDigests[0] = 0x5a; 255 | setUnsignedInt32LittleEngian( 256 | chunkCount, concatenationOfChunkCountAndChunkDigests, 1); 257 | digestsOfChunks.put(digestAlgorithm, concatenationOfChunkCountAndChunkDigests); 258 | } 259 | 260 | int chunkIndex = 0; 261 | byte[] chunkContentPrefix = new byte[5]; 262 | chunkContentPrefix[0] = (byte) 0xa5; 263 | // Optimization opportunity: digests of chunks can be computed in parallel. 264 | for (ByteBuffer input : contents) { 265 | while (input.hasRemaining()) { 266 | int chunkSize = 267 | Math.min(input.remaining(), CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES); 268 | final ByteBuffer chunk = getByteBuffer(input, chunkSize); 269 | for (int digestAlgorithm : digestAlgorithms) { 270 | String jcaAlgorithmName = 271 | getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); 272 | MessageDigest md; 273 | try { 274 | md = MessageDigest.getInstance(jcaAlgorithmName); 275 | } catch (NoSuchAlgorithmException e) { 276 | throw new DigestException( 277 | jcaAlgorithmName + " MessageDigest not supported", e); 278 | } 279 | // Reset position to 0 and limit to capacity. Position would've been modified 280 | // by the preceding iteration of this loop. NOTE: Contrary to the method name, 281 | // this does not modify the contents of the chunk. 282 | chunk.clear(); 283 | setUnsignedInt32LittleEngian(chunk.remaining(), chunkContentPrefix, 1); 284 | md.update(chunkContentPrefix); 285 | md.update(chunk); 286 | byte[] concatenationOfChunkCountAndChunkDigests = 287 | digestsOfChunks.get(digestAlgorithm); 288 | int expectedDigestSizeBytes = 289 | getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm); 290 | int actualDigestSizeBytes = 291 | md.digest( 292 | concatenationOfChunkCountAndChunkDigests, 293 | 5 + chunkIndex * expectedDigestSizeBytes, 294 | expectedDigestSizeBytes); 295 | if (actualDigestSizeBytes != expectedDigestSizeBytes) { 296 | throw new DigestException( 297 | "Unexpected output size of " + md.getAlgorithm() 298 | + " digest: " + actualDigestSizeBytes); 299 | } 300 | } 301 | chunkIndex++; 302 | } 303 | } 304 | 305 | Map result = new HashMap<>(digestAlgorithms.size()); 306 | for (Map.Entry entry : digestsOfChunks.entrySet()) { 307 | int digestAlgorithm = entry.getKey(); 308 | byte[] concatenationOfChunkCountAndChunkDigests = entry.getValue(); 309 | String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm); 310 | MessageDigest md; 311 | try { 312 | md = MessageDigest.getInstance(jcaAlgorithmName); 313 | } catch (NoSuchAlgorithmException e) { 314 | throw new DigestException(jcaAlgorithmName + " MessageDigest not supported", e); 315 | } 316 | result.put(digestAlgorithm, md.digest(concatenationOfChunkCountAndChunkDigests)); 317 | } 318 | return result; 319 | } 320 | 321 | private static int getChunkCount(int inputSize, int chunkSize) { 322 | return (inputSize + chunkSize - 1) / chunkSize; 323 | } 324 | 325 | private static void setUnsignedInt32LittleEngian(int value, byte[] result, int offset) { 326 | result[offset] = (byte) (value & 0xff); 327 | result[offset + 1] = (byte) ((value >> 8) & 0xff); 328 | result[offset + 2] = (byte) ((value >> 16) & 0xff); 329 | result[offset + 3] = (byte) ((value >> 24) & 0xff); 330 | } 331 | 332 | private static byte[] generateApkSigningBlock( 333 | List signerConfigs, 334 | Map contentDigests) throws InvalidKeyException, SignatureException { 335 | byte[] apkSignatureSchemeV2Block = 336 | generateApkSignatureSchemeV2Block(signerConfigs, contentDigests); 337 | return generateApkSigningBlock(apkSignatureSchemeV2Block); 338 | } 339 | 340 | private static byte[] generateApkSigningBlock(byte[] apkSignatureSchemeV2Block) { 341 | // FORMAT: 342 | // uint64: size (excluding this field) 343 | // repeated ID-value pairs: 344 | // uint64: size (excluding this field) 345 | // uint32: ID 346 | // (size - 4) bytes: value 347 | // uint64: size (same as the one above) 348 | // uint128: magic 349 | 350 | int resultSize = 351 | 8 // size 352 | + 8 + 4 + apkSignatureSchemeV2Block.length // v2Block as ID-value pair 353 | + 8 // size 354 | + 16 // magic 355 | ; 356 | ByteBuffer result = ByteBuffer.allocate(resultSize); 357 | result.order(ByteOrder.LITTLE_ENDIAN); 358 | long blockSizeFieldValue = resultSize - 8; 359 | result.putLong(blockSizeFieldValue); 360 | 361 | long pairSizeFieldValue = 4 + apkSignatureSchemeV2Block.length; 362 | result.putLong(pairSizeFieldValue); 363 | result.putInt(APK_SIGNATURE_SCHEME_V2_BLOCK_ID); 364 | result.put(apkSignatureSchemeV2Block); 365 | 366 | result.putLong(blockSizeFieldValue); 367 | result.put(APK_SIGNING_BLOCK_MAGIC); 368 | 369 | return result.array(); 370 | } 371 | 372 | private static byte[] generateApkSignatureSchemeV2Block( 373 | List signerConfigs, 374 | Map contentDigests) throws InvalidKeyException, SignatureException { 375 | // FORMAT: 376 | // * length-prefixed sequence of length-prefixed signer blocks. 377 | 378 | List signerBlocks = new ArrayList<>(signerConfigs.size()); 379 | int signerNumber = 0; 380 | for (SignerConfig signerConfig : signerConfigs) { 381 | signerNumber++; 382 | byte[] signerBlock; 383 | try { 384 | signerBlock = generateSignerBlock(signerConfig, contentDigests); 385 | } catch (InvalidKeyException e) { 386 | throw new InvalidKeyException("Signer #" + signerNumber + " failed", e); 387 | } catch (SignatureException e) { 388 | throw new SignatureException("Signer #" + signerNumber + " failed", e); 389 | } 390 | signerBlocks.add(signerBlock); 391 | } 392 | 393 | return encodeAsSequenceOfLengthPrefixedElements( 394 | new byte[][] { 395 | encodeAsSequenceOfLengthPrefixedElements(signerBlocks), 396 | }); 397 | } 398 | 399 | private static byte[] generateSignerBlock( 400 | SignerConfig signerConfig, 401 | Map contentDigests) throws InvalidKeyException, SignatureException { 402 | if (signerConfig.certificates.isEmpty()) { 403 | throw new SignatureException("No certificates configured for signer"); 404 | } 405 | PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey(); 406 | 407 | byte[] encodedPublicKey = encodePublicKey(publicKey); 408 | 409 | V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData(); 410 | try { 411 | signedData.certificates = encodeCertificates(signerConfig.certificates); 412 | } catch (CertificateEncodingException e) { 413 | throw new SignatureException("Failed to encode certificates", e); 414 | } 415 | 416 | List> digests = 417 | new ArrayList<>(signerConfig.signatureAlgorithms.size()); 418 | for (int signatureAlgorithm : signerConfig.signatureAlgorithms) { 419 | int contentDigestAlgorithm = 420 | getSignatureAlgorithmContentDigestAlgorithm(signatureAlgorithm); 421 | byte[] contentDigest = contentDigests.get(contentDigestAlgorithm); 422 | if (contentDigest == null) { 423 | throw new RuntimeException( 424 | getContentDigestAlgorithmJcaDigestAlgorithm(contentDigestAlgorithm) 425 | + " content digest for " 426 | + getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm) 427 | + " not computed"); 428 | } 429 | digests.add(Pair.create(signatureAlgorithm, contentDigest)); 430 | } 431 | signedData.digests = digests; 432 | 433 | V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer(); 434 | // FORMAT: 435 | // * length-prefixed sequence of length-prefixed digests: 436 | // * uint32: signature algorithm ID 437 | // * length-prefixed bytes: digest of contents 438 | // * length-prefixed sequence of certificates: 439 | // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). 440 | // * length-prefixed sequence of length-prefixed additional attributes: 441 | // * uint32: ID 442 | // * (length - 4) bytes: value 443 | signer.signedData = encodeAsSequenceOfLengthPrefixedElements(new byte[][] { 444 | encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(signedData.digests), 445 | encodeAsSequenceOfLengthPrefixedElements(signedData.certificates), 446 | // additional attributes 447 | new byte[0], 448 | }); 449 | signer.publicKey = encodedPublicKey; 450 | signer.signatures = new ArrayList<>(); 451 | for (int signatureAlgorithm : signerConfig.signatureAlgorithms) { 452 | Pair signatureParams = 453 | getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithm); 454 | String jcaSignatureAlgorithm = signatureParams.getFirst(); 455 | AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureParams.getSecond(); 456 | byte[] signatureBytes; 457 | try { 458 | Signature signature = Signature.getInstance(jcaSignatureAlgorithm); 459 | signature.initSign(signerConfig.privateKey); 460 | if (jcaSignatureAlgorithmParams != null) { 461 | signature.setParameter(jcaSignatureAlgorithmParams); 462 | } 463 | signature.update(signer.signedData); 464 | signatureBytes = signature.sign(); 465 | } catch (InvalidKeyException e) { 466 | throw new InvalidKeyException("Failed sign using " + jcaSignatureAlgorithm, e); 467 | } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException 468 | | SignatureException e) { 469 | throw new SignatureException("Failed sign using " + jcaSignatureAlgorithm, e); 470 | } 471 | 472 | try { 473 | Signature signature = Signature.getInstance(jcaSignatureAlgorithm); 474 | signature.initVerify(publicKey); 475 | if (jcaSignatureAlgorithmParams != null) { 476 | signature.setParameter(jcaSignatureAlgorithmParams); 477 | } 478 | signature.update(signer.signedData); 479 | if (!signature.verify(signatureBytes)) { 480 | throw new SignatureException("Signature did not verify"); 481 | } 482 | } catch (InvalidKeyException e) { 483 | throw new InvalidKeyException("Failed to verify generated " + jcaSignatureAlgorithm 484 | + " signature using public key from certificate", e); 485 | } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException 486 | | SignatureException e) { 487 | throw new SignatureException("Failed to verify generated " + jcaSignatureAlgorithm 488 | + " signature using public key from certificate", e); 489 | } 490 | 491 | signer.signatures.add(Pair.create(signatureAlgorithm, signatureBytes)); 492 | } 493 | 494 | // FORMAT: 495 | // * length-prefixed signed data 496 | // * length-prefixed sequence of length-prefixed signatures: 497 | // * uint32: signature algorithm ID 498 | // * length-prefixed bytes: signature of signed data 499 | // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded) 500 | return encodeAsSequenceOfLengthPrefixedElements( 501 | new byte[][] { 502 | signer.signedData, 503 | encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( 504 | signer.signatures), 505 | signer.publicKey, 506 | }); 507 | } 508 | 509 | private static final class V2SignatureSchemeBlock { 510 | private static final class Signer { 511 | public byte[] signedData; 512 | public List> signatures; 513 | public byte[] publicKey; 514 | } 515 | 516 | private static final class SignedData { 517 | public List> digests; 518 | public List certificates; 519 | } 520 | } 521 | 522 | private static byte[] encodePublicKey(PublicKey publicKey) throws InvalidKeyException { 523 | byte[] encodedPublicKey = null; 524 | if ("X.509".equals(publicKey.getFormat())) { 525 | encodedPublicKey = publicKey.getEncoded(); 526 | } 527 | if (encodedPublicKey == null) { 528 | try { 529 | encodedPublicKey = 530 | KeyFactory.getInstance(publicKey.getAlgorithm()) 531 | .getKeySpec(publicKey, X509EncodedKeySpec.class) 532 | .getEncoded(); 533 | } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { 534 | throw new InvalidKeyException( 535 | "Failed to obtain X.509 encoded form of public key " + publicKey 536 | + " of class " + publicKey.getClass().getName(), 537 | e); 538 | } 539 | } 540 | if ((encodedPublicKey == null) || (encodedPublicKey.length == 0)) { 541 | throw new InvalidKeyException( 542 | "Failed to obtain X.509 encoded form of public key " + publicKey 543 | + " of class " + publicKey.getClass().getName()); 544 | } 545 | return encodedPublicKey; 546 | } 547 | 548 | public static List encodeCertificates(List certificates) 549 | throws CertificateEncodingException { 550 | List result = new ArrayList<>(); 551 | for (X509Certificate certificate : certificates) { 552 | result.add(certificate.getEncoded()); 553 | } 554 | return result; 555 | } 556 | 557 | private static byte[] encodeAsSequenceOfLengthPrefixedElements(List sequence) { 558 | return encodeAsSequenceOfLengthPrefixedElements( 559 | sequence.toArray(new byte[sequence.size()][])); 560 | } 561 | 562 | private static byte[] encodeAsSequenceOfLengthPrefixedElements(byte[][] sequence) { 563 | int payloadSize = 0; 564 | for (byte[] element : sequence) { 565 | payloadSize += 4 + element.length; 566 | } 567 | ByteBuffer result = ByteBuffer.allocate(payloadSize); 568 | result.order(ByteOrder.LITTLE_ENDIAN); 569 | for (byte[] element : sequence) { 570 | result.putInt(element.length); 571 | result.put(element); 572 | } 573 | return result.array(); 574 | } 575 | 576 | private static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( 577 | List> sequence) { 578 | int resultSize = 0; 579 | for (Pair element : sequence) { 580 | resultSize += 12 + element.getSecond().length; 581 | } 582 | ByteBuffer result = ByteBuffer.allocate(resultSize); 583 | result.order(ByteOrder.LITTLE_ENDIAN); 584 | for (Pair element : sequence) { 585 | byte[] second = element.getSecond(); 586 | result.putInt(8 + second.length); 587 | result.putInt(element.getFirst()); 588 | result.putInt(second.length); 589 | result.put(second); 590 | } 591 | return result.array(); 592 | } 593 | 594 | /** 595 | * Relative get method for reading {@code size} number of bytes from the current 596 | * position of this buffer. 597 | * 598 | *

This method reads the next {@code size} bytes at this buffer's current position, 599 | * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to 600 | * {@code size}, byte order set to this buffer's byte order; and then increments the position by 601 | * {@code size}. 602 | */ 603 | private static ByteBuffer getByteBuffer(ByteBuffer source, int size) { 604 | if (size < 0) { 605 | throw new IllegalArgumentException("size: " + size); 606 | } 607 | int originalLimit = source.limit(); 608 | int position = source.position(); 609 | int limit = position + size; 610 | if ((limit < position) || (limit > originalLimit)) { 611 | throw new BufferUnderflowException(); 612 | } 613 | source.limit(limit); 614 | try { 615 | ByteBuffer result = source.slice(); 616 | result.order(source.order()); 617 | source.position(limit); 618 | return result; 619 | } finally { 620 | source.limit(originalLimit); 621 | } 622 | } 623 | 624 | private static Pair 625 | getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) { 626 | switch (sigAlgorithm) { 627 | case SIGNATURE_RSA_PSS_WITH_SHA256: 628 | return Pair.create( 629 | "SHA256withRSA/PSS", 630 | new PSSParameterSpec( 631 | "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)); 632 | case SIGNATURE_RSA_PSS_WITH_SHA512: 633 | return Pair.create( 634 | "SHA512withRSA/PSS", 635 | new PSSParameterSpec( 636 | "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)); 637 | case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: 638 | return Pair.create("SHA256withRSA", null); 639 | case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: 640 | return Pair.create("SHA512withRSA", null); 641 | case SIGNATURE_ECDSA_WITH_SHA256: 642 | return Pair.create("SHA256withECDSA", null); 643 | case SIGNATURE_ECDSA_WITH_SHA512: 644 | return Pair.create("SHA512withECDSA", null); 645 | case SIGNATURE_DSA_WITH_SHA256: 646 | return Pair.create("SHA256withDSA", null); 647 | case SIGNATURE_DSA_WITH_SHA512: 648 | return Pair.create("SHA512withDSA", null); 649 | default: 650 | throw new IllegalArgumentException( 651 | "Unknown signature algorithm: 0x" 652 | + Long.toHexString(sigAlgorithm & 0xffffffff)); 653 | } 654 | } 655 | 656 | private static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) { 657 | switch (sigAlgorithm) { 658 | case SIGNATURE_RSA_PSS_WITH_SHA256: 659 | case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256: 660 | case SIGNATURE_ECDSA_WITH_SHA256: 661 | case SIGNATURE_DSA_WITH_SHA256: 662 | return CONTENT_DIGEST_CHUNKED_SHA256; 663 | case SIGNATURE_RSA_PSS_WITH_SHA512: 664 | case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512: 665 | case SIGNATURE_ECDSA_WITH_SHA512: 666 | case SIGNATURE_DSA_WITH_SHA512: 667 | return CONTENT_DIGEST_CHUNKED_SHA512; 668 | default: 669 | throw new IllegalArgumentException( 670 | "Unknown signature algorithm: 0x" 671 | + Long.toHexString(sigAlgorithm & 0xffffffff)); 672 | } 673 | } 674 | 675 | private static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) { 676 | switch (digestAlgorithm) { 677 | case CONTENT_DIGEST_CHUNKED_SHA256: 678 | return "SHA-256"; 679 | case CONTENT_DIGEST_CHUNKED_SHA512: 680 | return "SHA-512"; 681 | default: 682 | throw new IllegalArgumentException( 683 | "Unknown content digest algorthm: " + digestAlgorithm); 684 | } 685 | } 686 | 687 | private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) { 688 | switch (digestAlgorithm) { 689 | case CONTENT_DIGEST_CHUNKED_SHA256: 690 | return 256 / 8; 691 | case CONTENT_DIGEST_CHUNKED_SHA512: 692 | return 512 / 8; 693 | default: 694 | throw new IllegalArgumentException( 695 | "Unknown content digest algorthm: " + digestAlgorithm); 696 | } 697 | } 698 | 699 | /** 700 | * Indicates that APK file could not be parsed. 701 | */ 702 | public static class ApkParseException extends Exception { 703 | private static final long serialVersionUID = 1L; 704 | 705 | public ApkParseException(String message) { 706 | super(message); 707 | } 708 | 709 | public ApkParseException(String message, Throwable cause) { 710 | super(message, cause); 711 | } 712 | } 713 | 714 | /** 715 | * Pair of two elements. 716 | */ 717 | private static class Pair { 718 | private final A mFirst; 719 | private final B mSecond; 720 | 721 | private Pair(A first, B second) { 722 | mFirst = first; 723 | mSecond = second; 724 | } 725 | 726 | public static Pair create(A first, B second) { 727 | return new Pair<>(first, second); 728 | } 729 | 730 | public A getFirst() { 731 | return mFirst; 732 | } 733 | 734 | public B getSecond() { 735 | return mSecond; 736 | } 737 | 738 | @Override 739 | public int hashCode() { 740 | final int prime = 31; 741 | int result = 1; 742 | result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode()); 743 | result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode()); 744 | return result; 745 | } 746 | 747 | @Override 748 | public boolean equals(Object obj) { 749 | if (this == obj) { 750 | return true; 751 | } 752 | if (obj == null) { 753 | return false; 754 | } 755 | if (getClass() != obj.getClass()) { 756 | return false; 757 | } 758 | @SuppressWarnings("rawtypes") 759 | Pair other = (Pair) obj; 760 | if (mFirst == null) { 761 | if (other.mFirst != null) { 762 | return false; 763 | } 764 | } else if (!mFirst.equals(other.mFirst)) { 765 | return false; 766 | } 767 | if (mSecond == null) { 768 | return other.mSecond == null; 769 | } else return mSecond.equals(other.mSecond); 770 | } 771 | } 772 | } 773 | -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/hideapk/signing/ByteArrayStream.java: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk.signing; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | public class ByteArrayStream extends ByteArrayOutputStream { 9 | 10 | public synchronized void readFrom(InputStream is) { 11 | readFrom(is, Integer.MAX_VALUE); 12 | } 13 | 14 | public synchronized void readFrom(InputStream is, int len) { 15 | int read; 16 | byte buffer[] = new byte[4096]; 17 | try { 18 | while ((read = is.read(buffer, 0, Math.min(len, buffer.length))) > 0) { 19 | write(buffer, 0, read); 20 | len -= read; 21 | } 22 | } catch (IOException e) { 23 | e.printStackTrace(); 24 | } 25 | } 26 | 27 | public ByteArrayInputStream getInputStream() { 28 | return new ByteArrayInputStream(buf, 0, count); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/hideapk/signing/CryptoUtils.java: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk.signing; 2 | 3 | import org.bouncycastle.asn1.ASN1InputStream; 4 | import org.bouncycastle.asn1.ASN1ObjectIdentifier; 5 | import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; 6 | import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; 7 | import org.bouncycastle.asn1.x509.AlgorithmIdentifier; 8 | import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; 9 | 10 | import java.io.ByteArrayInputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.security.GeneralSecurityException; 14 | import java.security.Key; 15 | import java.security.KeyFactory; 16 | import java.security.PrivateKey; 17 | import java.security.PublicKey; 18 | import java.security.cert.CertificateFactory; 19 | import java.security.cert.X509Certificate; 20 | import java.security.spec.ECPrivateKeySpec; 21 | import java.security.spec.ECPublicKeySpec; 22 | import java.security.spec.InvalidKeySpecException; 23 | import java.security.spec.PKCS8EncodedKeySpec; 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | 27 | public class CryptoUtils { 28 | 29 | static final Map ID_TO_ALG; 30 | static final Map ALG_TO_ID; 31 | 32 | static { 33 | ID_TO_ALG = new HashMap<>(); 34 | ALG_TO_ID = new HashMap<>(); 35 | ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA256.getId(), "SHA256withECDSA"); 36 | ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA384.getId(), "SHA384withECDSA"); 37 | ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA512.getId(), "SHA512withECDSA"); 38 | ID_TO_ALG.put(PKCSObjectIdentifiers.sha1WithRSAEncryption.getId(), "SHA1withRSA"); 39 | ID_TO_ALG.put(PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(), "SHA256withRSA"); 40 | ID_TO_ALG.put(PKCSObjectIdentifiers.sha512WithRSAEncryption.getId(), "SHA512withRSA"); 41 | ALG_TO_ID.put("SHA256withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA256.getId()); 42 | ALG_TO_ID.put("SHA384withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA384.getId()); 43 | ALG_TO_ID.put("SHA512withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA512.getId()); 44 | ALG_TO_ID.put("SHA1withRSA", PKCSObjectIdentifiers.sha1WithRSAEncryption.getId()); 45 | ALG_TO_ID.put("SHA256withRSA", PKCSObjectIdentifiers.sha256WithRSAEncryption.getId()); 46 | ALG_TO_ID.put("SHA512withRSA", PKCSObjectIdentifiers.sha512WithRSAEncryption.getId()); 47 | } 48 | 49 | static String getSignatureAlgorithm(Key key) throws Exception { 50 | if ("EC".equals(key.getAlgorithm())) { 51 | int curveSize; 52 | KeyFactory factory = KeyFactory.getInstance("EC"); 53 | if (key instanceof PublicKey) { 54 | ECPublicKeySpec spec = factory.getKeySpec(key, ECPublicKeySpec.class); 55 | curveSize = spec.getParams().getCurve().getField().getFieldSize(); 56 | } else if (key instanceof PrivateKey) { 57 | ECPrivateKeySpec spec = factory.getKeySpec(key, ECPrivateKeySpec.class); 58 | curveSize = spec.getParams().getCurve().getField().getFieldSize(); 59 | } else { 60 | throw new InvalidKeySpecException(); 61 | } 62 | if (curveSize <= 256) { 63 | return "SHA256withECDSA"; 64 | } else if (curveSize <= 384) { 65 | return "SHA384withECDSA"; 66 | } else { 67 | return "SHA512withECDSA"; 68 | } 69 | } else if ("RSA".equals(key.getAlgorithm())) { 70 | return "SHA256withRSA"; 71 | } else { 72 | throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm()); 73 | } 74 | } 75 | 76 | static AlgorithmIdentifier getSignatureAlgorithmIdentifier(Key key) throws Exception { 77 | String id = ALG_TO_ID.get(getSignatureAlgorithm(key)); 78 | if (id == null) { 79 | throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm()); 80 | } 81 | return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id)); 82 | } 83 | 84 | public static X509Certificate readCertificate(InputStream input) 85 | throws IOException, GeneralSecurityException { 86 | try { 87 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 88 | return (X509Certificate) cf.generateCertificate(input); 89 | } finally { 90 | input.close(); 91 | } 92 | } 93 | 94 | /** Read a PKCS#8 format private key. */ 95 | public static PrivateKey readPrivateKey(InputStream input) 96 | throws IOException, GeneralSecurityException { 97 | try { 98 | ByteArrayStream buf = new ByteArrayStream(); 99 | buf.readFrom(input); 100 | byte[] bytes = buf.toByteArray(); 101 | /* Check to see if this is in an EncryptedPrivateKeyInfo structure. */ 102 | PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes); 103 | /* 104 | * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm 105 | * OID and use that to construct a KeyFactory. 106 | */ 107 | ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded())); 108 | PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject()); 109 | String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); 110 | return KeyFactory.getInstance(algOid).generatePrivate(spec); 111 | } finally { 112 | input.close(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/hideapk/signing/JarMap.java: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk.signing; 2 | 3 | import java.io.Closeable; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.util.Collections; 9 | import java.util.Enumeration; 10 | import java.util.LinkedHashMap; 11 | import java.util.jar.JarEntry; 12 | import java.util.jar.JarFile; 13 | import java.util.jar.JarInputStream; 14 | import java.util.jar.Manifest; 15 | import java.util.zip.ZipEntry; 16 | import java.util.zip.ZipFile; 17 | 18 | public abstract class JarMap implements Closeable { 19 | 20 | LinkedHashMap entryMap; 21 | 22 | public static JarMap open(File file, boolean verify) throws IOException { 23 | return new FileMap(file, verify, ZipFile.OPEN_READ); 24 | } 25 | 26 | public static JarMap open(InputStream is, boolean verify) throws IOException { 27 | return new StreamMap(is, verify); 28 | } 29 | 30 | public File getFile() { 31 | return null; 32 | } 33 | 34 | public abstract Manifest getManifest() throws IOException; 35 | 36 | public InputStream getInputStream(ZipEntry ze) throws IOException { 37 | JarMapEntry e = getMapEntry(ze.getName()); 38 | return e != null ? e.data.getInputStream() : null; 39 | } 40 | 41 | public OutputStream getOutputStream(ZipEntry ze) { 42 | if (entryMap == null) 43 | entryMap = new LinkedHashMap<>(); 44 | JarMapEntry e = new JarMapEntry(ze.getName()); 45 | entryMap.put(ze.getName(), e); 46 | return e.data; 47 | } 48 | 49 | public byte[] getRawData(ZipEntry ze) throws IOException { 50 | JarMapEntry e = getMapEntry(ze.getName()); 51 | return e != null ? e.data.toByteArray() : null; 52 | } 53 | 54 | public abstract Enumeration entries(); 55 | 56 | public final ZipEntry getEntry(String name) { 57 | return getJarEntry(name); 58 | } 59 | 60 | public JarEntry getJarEntry(String name) { 61 | return getMapEntry(name); 62 | } 63 | 64 | JarMapEntry getMapEntry(String name) { 65 | JarMapEntry e = null; 66 | if (entryMap != null) 67 | e = (JarMapEntry) entryMap.get(name); 68 | return e; 69 | } 70 | 71 | private static class FileMap extends JarMap { 72 | 73 | private JarFile jarFile; 74 | 75 | FileMap(File file, boolean verify, int mode) throws IOException { 76 | jarFile = new JarFile(file, verify, mode); 77 | } 78 | 79 | @Override 80 | public File getFile() { 81 | return new File(jarFile.getName()); 82 | } 83 | 84 | @Override 85 | public Manifest getManifest() throws IOException { 86 | return jarFile.getManifest(); 87 | } 88 | 89 | @Override 90 | public InputStream getInputStream(ZipEntry ze) throws IOException { 91 | InputStream is = super.getInputStream(ze); 92 | return is != null ? is : jarFile.getInputStream(ze); 93 | } 94 | 95 | @Override 96 | public byte[] getRawData(ZipEntry ze) throws IOException { 97 | byte[] b = super.getRawData(ze); 98 | if (b != null) 99 | return b; 100 | ByteArrayStream bytes = new ByteArrayStream(); 101 | bytes.readFrom(jarFile.getInputStream(ze)); 102 | return bytes.toByteArray(); 103 | } 104 | 105 | @Override 106 | public Enumeration entries() { 107 | return jarFile.entries(); 108 | } 109 | 110 | @Override 111 | public JarEntry getJarEntry(String name) { 112 | JarEntry e = getMapEntry(name); 113 | return e != null ? e : jarFile.getJarEntry(name); 114 | } 115 | 116 | @Override 117 | public void close() throws IOException { 118 | jarFile.close(); 119 | } 120 | } 121 | 122 | private static class StreamMap extends JarMap { 123 | 124 | private JarInputStream jis; 125 | 126 | StreamMap(InputStream is, boolean verify) throws IOException { 127 | jis = new JarInputStream(is, verify); 128 | entryMap = new LinkedHashMap<>(); 129 | JarEntry entry; 130 | while ((entry = jis.getNextJarEntry()) != null) { 131 | entryMap.put(entry.getName(), new JarMapEntry(entry, jis)); 132 | } 133 | } 134 | 135 | @Override 136 | public Manifest getManifest() { 137 | return jis.getManifest(); 138 | } 139 | 140 | @Override 141 | public Enumeration entries() { 142 | return Collections.enumeration(entryMap.values()); 143 | } 144 | 145 | @Override 146 | public void close() throws IOException { 147 | jis.close(); 148 | } 149 | } 150 | 151 | private static class JarMapEntry extends JarEntry { 152 | 153 | ByteArrayStream data; 154 | 155 | JarMapEntry(JarEntry je, InputStream is) { 156 | super(je); 157 | data = new ByteArrayStream(); 158 | data.readFrom(is); 159 | } 160 | 161 | JarMapEntry(String s) { 162 | super(s); 163 | data = new ByteArrayStream(); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/hideapk/signing/SignApk.java: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk.signing; 2 | 3 | import org.bouncycastle.asn1.ASN1Encoding; 4 | import org.bouncycastle.asn1.ASN1InputStream; 5 | import org.bouncycastle.asn1.ASN1OutputStream; 6 | import org.bouncycastle.cert.jcajce.JcaCertStore; 7 | import org.bouncycastle.cms.CMSException; 8 | import org.bouncycastle.cms.CMSProcessableByteArray; 9 | import org.bouncycastle.cms.CMSSignedData; 10 | import org.bouncycastle.cms.CMSSignedDataGenerator; 11 | import org.bouncycastle.cms.CMSTypedData; 12 | import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; 13 | import org.bouncycastle.operator.ContentSigner; 14 | import org.bouncycastle.operator.OperatorCreationException; 15 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; 16 | import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; 17 | import org.bouncycastle.util.encoders.Base64; 18 | 19 | import java.io.ByteArrayOutputStream; 20 | import java.io.FilterOutputStream; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.io.OutputStream; 24 | import java.io.PrintStream; 25 | import java.nio.ByteBuffer; 26 | import java.security.DigestOutputStream; 27 | import java.security.GeneralSecurityException; 28 | import java.security.InvalidKeyException; 29 | import java.security.MessageDigest; 30 | import java.security.PrivateKey; 31 | import java.security.PublicKey; 32 | import java.security.cert.CertificateEncodingException; 33 | import java.security.cert.X509Certificate; 34 | import java.util.ArrayList; 35 | import java.util.Collections; 36 | import java.util.Enumeration; 37 | import java.util.Iterator; 38 | import java.util.List; 39 | import java.util.Locale; 40 | import java.util.Map; 41 | import java.util.TimeZone; 42 | import java.util.TreeMap; 43 | import java.util.jar.Attributes; 44 | import java.util.jar.JarEntry; 45 | import java.util.jar.JarFile; 46 | import java.util.jar.JarOutputStream; 47 | import java.util.jar.Manifest; 48 | import java.util.regex.Pattern; 49 | 50 | /* 51 | * Modified from from AOSP 52 | * https://android.googlesource.com/platform/build/+/refs/tags/android-7.1.2_r39/tools/signapk/src/com/android/signapk/SignApk.java 53 | * */ 54 | 55 | public class SignApk { 56 | private static final String CERT_SF_NAME = "META-INF/CERT.SF"; 57 | private static final String CERT_SIG_NAME = "META-INF/CERT.%s"; 58 | private static final String CERT_SF_MULTI_NAME = "META-INF/CERT%d.SF"; 59 | private static final String CERT_SIG_MULTI_NAME = "META-INF/CERT%d.%s"; 60 | 61 | // bitmasks for which hash algorithms we need the manifest to include. 62 | private static final int USE_SHA1 = 1; 63 | private static final int USE_SHA256 = 2; 64 | 65 | /** 66 | * Digest algorithm used when signing the APK using APK Signature Scheme v2. 67 | */ 68 | private static final String APK_SIG_SCHEME_V2_DIGEST_ALGORITHM = "SHA-256"; 69 | // Files matching this pattern are not copied to the output. 70 | private static final Pattern stripPattern = 71 | Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" + 72 | Pattern.quote(JarFile.MANIFEST_NAME) + ")$"); 73 | 74 | /** 75 | * Return one of USE_SHA1 or USE_SHA256 according to the signature 76 | * algorithm specified in the cert. 77 | */ 78 | private static int getDigestAlgorithm(X509Certificate cert) { 79 | String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US); 80 | if ("SHA1WITHRSA".equals(sigAlg) || "MD5WITHRSA".equals(sigAlg)) { 81 | return USE_SHA1; 82 | } else if (sigAlg.startsWith("SHA256WITH")) { 83 | return USE_SHA256; 84 | } else { 85 | throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg + 86 | "\" in cert [" + cert.getSubjectDN()); 87 | } 88 | } 89 | 90 | /** 91 | * Returns the expected signature algorithm for this key type. 92 | */ 93 | private static String getSignatureAlgorithm(X509Certificate cert) { 94 | String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US); 95 | if ("RSA".equalsIgnoreCase(keyType)) { 96 | if (getDigestAlgorithm(cert) == USE_SHA256) { 97 | return "SHA256withRSA"; 98 | } else { 99 | return "SHA1withRSA"; 100 | } 101 | } else if ("EC".equalsIgnoreCase(keyType)) { 102 | return "SHA256withECDSA"; 103 | } else { 104 | throw new IllegalArgumentException("unsupported key type: " + keyType); 105 | } 106 | } 107 | 108 | /** 109 | * Add the hash(es) of every file to the manifest, creating it if 110 | * necessary. 111 | */ 112 | private static Manifest addDigestsToManifest(JarMap jar, int hashes) 113 | throws IOException, GeneralSecurityException { 114 | Manifest input = jar.getManifest(); 115 | Manifest output = new Manifest(); 116 | Attributes main = output.getMainAttributes(); 117 | if (input != null) { 118 | main.putAll(input.getMainAttributes()); 119 | } else { 120 | main.putValue("Manifest-Version", "1.0"); 121 | main.putValue("Created-By", "1.0 (Android SignApk)"); 122 | } 123 | 124 | MessageDigest md_sha1 = null; 125 | MessageDigest md_sha256 = null; 126 | if ((hashes & USE_SHA1) != 0) { 127 | md_sha1 = MessageDigest.getInstance("SHA1"); 128 | } 129 | if ((hashes & USE_SHA256) != 0) { 130 | md_sha256 = MessageDigest.getInstance("SHA256"); 131 | } 132 | 133 | byte[] buffer = new byte[4096]; 134 | int num; 135 | 136 | // We sort the input entries by name, and add them to the 137 | // output manifest in sorted order. We expect that the output 138 | // map will be deterministic. 139 | 140 | TreeMap byName = new TreeMap<>(); 141 | 142 | for (Enumeration e = jar.entries(); e.hasMoreElements(); ) { 143 | JarEntry entry = e.nextElement(); 144 | byName.put(entry.getName(), entry); 145 | } 146 | 147 | for (JarEntry entry : byName.values()) { 148 | String name = entry.getName(); 149 | if (!entry.isDirectory() && !stripPattern.matcher(name).matches()) { 150 | InputStream data = jar.getInputStream(entry); 151 | while ((num = data.read(buffer)) > 0) { 152 | if (md_sha1 != null) md_sha1.update(buffer, 0, num); 153 | if (md_sha256 != null) md_sha256.update(buffer, 0, num); 154 | } 155 | 156 | Attributes attr = null; 157 | if (input != null) attr = input.getAttributes(name); 158 | attr = attr != null ? new Attributes(attr) : new Attributes(); 159 | // Remove any previously computed digests from this entry's attributes. 160 | for (Iterator i = attr.keySet().iterator(); i.hasNext(); ) { 161 | Object key = i.next(); 162 | if (!(key instanceof Attributes.Name)) { 163 | continue; 164 | } 165 | String attributeNameLowerCase = 166 | key.toString().toLowerCase(Locale.US); 167 | if (attributeNameLowerCase.endsWith("-digest")) { 168 | i.remove(); 169 | } 170 | } 171 | // Add SHA-1 digest if requested 172 | if (md_sha1 != null) { 173 | attr.putValue("SHA1-Digest", 174 | new String(Base64.encode(md_sha1.digest()), "ASCII")); 175 | } 176 | // Add SHA-256 digest if requested 177 | if (md_sha256 != null) { 178 | attr.putValue("SHA-256-Digest", 179 | new String(Base64.encode(md_sha256.digest()), "ASCII")); 180 | } 181 | output.getEntries().put(name, attr); 182 | } 183 | } 184 | 185 | return output; 186 | } 187 | 188 | /** 189 | * Write a .SF file with a digest of the specified manifest. 190 | */ 191 | private static void writeSignatureFile(Manifest manifest, OutputStream out, 192 | int hash) 193 | throws IOException, GeneralSecurityException { 194 | Manifest sf = new Manifest(); 195 | Attributes main = sf.getMainAttributes(); 196 | main.putValue("Signature-Version", "1.0"); 197 | main.putValue("Created-By", "1.0 (Android SignApk)"); 198 | // Add APK Signature Scheme v2 signature stripping protection. 199 | // This attribute indicates that this APK is supposed to have been signed using one or 200 | // more APK-specific signature schemes in addition to the standard JAR signature scheme 201 | // used by this code. APK signature verifier should reject the APK if it does not 202 | // contain a signature for the signature scheme the verifier prefers out of this set. 203 | main.putValue( 204 | ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME, 205 | ApkSignerV2.SF_ATTRIBUTE_ANDROID_APK_SIGNED_VALUE); 206 | 207 | MessageDigest md = MessageDigest.getInstance(hash == USE_SHA256 ? "SHA256" : "SHA1"); 208 | PrintStream print = new PrintStream(new DigestOutputStream(new ByteArrayOutputStream(), md), 209 | true, "UTF-8"); 210 | 211 | // Digest of the entire manifest 212 | manifest.write(print); 213 | print.flush(); 214 | main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest", 215 | new String(Base64.encode(md.digest()), "ASCII")); 216 | 217 | Map entries = manifest.getEntries(); 218 | for (Map.Entry entry : entries.entrySet()) { 219 | // Digest of the manifest stanza for this entry. 220 | print.print("Name: " + entry.getKey() + "\r\n"); 221 | for (Map.Entry att : entry.getValue().entrySet()) { 222 | print.print(att.getKey() + ": " + att.getValue() + "\r\n"); 223 | } 224 | print.print("\r\n"); 225 | print.flush(); 226 | 227 | Attributes sfAttr = new Attributes(); 228 | sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest", 229 | new String(Base64.encode(md.digest()), "ASCII")); 230 | sf.getEntries().put(entry.getKey(), sfAttr); 231 | } 232 | 233 | CountOutputStream cout = new CountOutputStream(out); 234 | sf.write(cout); 235 | 236 | // A bug in the java.util.jar implementation of Android platforms 237 | // up to version 1.6 will cause a spurious IOException to be thrown 238 | // if the length of the signature file is a multiple of 1024 bytes. 239 | // As a workaround, add an extra CRLF in this case. 240 | if ((cout.size() % 1024) == 0) { 241 | cout.write('\r'); 242 | cout.write('\n'); 243 | } 244 | } 245 | 246 | /** 247 | * Sign data and write the digital signature to 'out'. 248 | */ 249 | private static void writeSignatureBlock( 250 | CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out) 251 | throws IOException, 252 | CertificateEncodingException, 253 | OperatorCreationException, 254 | CMSException { 255 | ArrayList certList = new ArrayList<>(1); 256 | certList.add(publicKey); 257 | JcaCertStore certs = new JcaCertStore(certList); 258 | 259 | CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); 260 | ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey)) 261 | .build(privateKey); 262 | gen.addSignerInfoGenerator( 263 | new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()) 264 | .setDirectSignature(true) 265 | .build(signer, publicKey) 266 | ); 267 | gen.addCertificates(certs); 268 | CMSSignedData sigData = gen.generate(data, false); 269 | 270 | try (ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded())) { 271 | ASN1OutputStream dos = ASN1OutputStream.create(out, ASN1Encoding.DER); 272 | dos.writeObject(asn1.readObject()); 273 | } 274 | } 275 | 276 | /** 277 | * Copy all the files in a manifest from input to output. We set 278 | * the modification times in the output to a fixed time, so as to 279 | * reduce variation in the output file and make incremental OTAs 280 | * more efficient. 281 | */ 282 | private static void copyFiles(Manifest manifest, JarMap in, JarOutputStream out, 283 | long timestamp, int defaultAlignment) throws IOException { 284 | byte[] buffer = new byte[4096]; 285 | int num; 286 | 287 | Map entries = manifest.getEntries(); 288 | ArrayList names = new ArrayList<>(entries.keySet()); 289 | Collections.sort(names); 290 | 291 | boolean firstEntry = true; 292 | long offset = 0L; 293 | 294 | // We do the copy in two passes -- first copying all the 295 | // entries that are STORED, then copying all the entries that 296 | // have any other compression flag (which in practice means 297 | // DEFLATED). This groups all the stored entries together at 298 | // the start of the file and makes it easier to do alignment 299 | // on them (since only stored entries are aligned). 300 | 301 | for (String name : names) { 302 | JarEntry inEntry = in.getJarEntry(name); 303 | JarEntry outEntry; 304 | if (inEntry.getMethod() != JarEntry.STORED) continue; 305 | // Preserve the STORED method of the input entry. 306 | outEntry = new JarEntry(inEntry); 307 | outEntry.setTime(timestamp); 308 | // Discard comment and extra fields of this entry to 309 | // simplify alignment logic below and for consistency with 310 | // how compressed entries are handled later. 311 | outEntry.setComment(null); 312 | outEntry.setExtra(null); 313 | 314 | // 'offset' is the offset into the file at which we expect 315 | // the file data to begin. This is the value we need to 316 | // make a multiple of 'alignement'. 317 | offset += JarFile.LOCHDR + outEntry.getName().length(); 318 | if (firstEntry) { 319 | // The first entry in a jar file has an extra field of 320 | // four bytes that you can't get rid of; any extra 321 | // data you specify in the JarEntry is appended to 322 | // these forced four bytes. This is JAR_MAGIC in 323 | // JarOutputStream; the bytes are 0xfeca0000. 324 | offset += 4; 325 | firstEntry = false; 326 | } 327 | int alignment = getStoredEntryDataAlignment(name, defaultAlignment); 328 | if (alignment > 0 && (offset % alignment != 0)) { 329 | // Set the "extra data" of the entry to between 1 and 330 | // alignment-1 bytes, to make the file data begin at 331 | // an aligned offset. 332 | int needed = alignment - (int) (offset % alignment); 333 | outEntry.setExtra(new byte[needed]); 334 | offset += needed; 335 | } 336 | 337 | out.putNextEntry(outEntry); 338 | 339 | InputStream data = in.getInputStream(inEntry); 340 | while ((num = data.read(buffer)) > 0) { 341 | out.write(buffer, 0, num); 342 | offset += num; 343 | } 344 | out.flush(); 345 | } 346 | 347 | // Copy all the non-STORED entries. We don't attempt to 348 | // maintain the 'offset' variable past this point; we don't do 349 | // alignment on these entries. 350 | 351 | for (String name : names) { 352 | JarEntry inEntry = in.getJarEntry(name); 353 | JarEntry outEntry; 354 | if (inEntry.getMethod() == JarEntry.STORED) continue; 355 | // Create a new entry so that the compressed len is recomputed. 356 | outEntry = new JarEntry(name); 357 | outEntry.setTime(timestamp); 358 | out.putNextEntry(outEntry); 359 | 360 | InputStream data = in.getInputStream(inEntry); 361 | while ((num = data.read(buffer)) > 0) { 362 | out.write(buffer, 0, num); 363 | } 364 | out.flush(); 365 | } 366 | } 367 | 368 | /** 369 | * Returns the multiple (in bytes) at which the provided {@code STORED} entry's data must start 370 | * relative to start of file or {@code 0} if alignment of this entry's data is not important. 371 | */ 372 | private static int getStoredEntryDataAlignment(String entryName, int defaultAlignment) { 373 | if (defaultAlignment <= 0) { 374 | return 0; 375 | } 376 | 377 | if (entryName.endsWith(".so")) { 378 | // Align .so contents to memory page boundary to enable memory-mapped 379 | // execution. 380 | return 4096; 381 | } else { 382 | return defaultAlignment; 383 | } 384 | } 385 | 386 | private static void signFile(Manifest manifest, 387 | X509Certificate[] publicKey, PrivateKey[] privateKey, 388 | long timestamp, JarOutputStream outputJar) throws Exception { 389 | // MANIFEST.MF 390 | JarEntry je = new JarEntry(JarFile.MANIFEST_NAME); 391 | je.setTime(timestamp); 392 | outputJar.putNextEntry(je); 393 | manifest.write(outputJar); 394 | 395 | int numKeys = publicKey.length; 396 | for (int k = 0; k < numKeys; ++k) { 397 | // CERT.SF / CERT#.SF 398 | je = new JarEntry(numKeys == 1 ? CERT_SF_NAME : 399 | (String.format(Locale.US, CERT_SF_MULTI_NAME, k))); 400 | je.setTime(timestamp); 401 | outputJar.putNextEntry(je); 402 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 403 | writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey[k])); 404 | byte[] signedData = baos.toByteArray(); 405 | outputJar.write(signedData); 406 | 407 | // CERT.{EC,RSA} / CERT#.{EC,RSA} 408 | final String keyType = publicKey[k].getPublicKey().getAlgorithm(); 409 | je = new JarEntry(numKeys == 1 ? (String.format(CERT_SIG_NAME, keyType)) : 410 | (String.format(Locale.US, CERT_SIG_MULTI_NAME, k, keyType))); 411 | je.setTime(timestamp); 412 | outputJar.putNextEntry(je); 413 | writeSignatureBlock(new CMSProcessableByteArray(signedData), 414 | publicKey[k], privateKey[k], outputJar); 415 | } 416 | } 417 | 418 | /** 419 | * Converts the provided lists of private keys, their X.509 certificates, and digest algorithms 420 | * into a list of APK Signature Scheme v2 {@code SignerConfig} instances. 421 | */ 422 | private static List createV2SignerConfigs( 423 | PrivateKey[] privateKeys, X509Certificate[] certificates, String[] digestAlgorithms) 424 | throws InvalidKeyException { 425 | if (privateKeys.length != certificates.length) { 426 | throw new IllegalArgumentException( 427 | "The number of private keys must match the number of certificates: " 428 | + privateKeys.length + " vs" + certificates.length); 429 | } 430 | List result = new ArrayList<>(privateKeys.length); 431 | for (int i = 0; i < privateKeys.length; i++) { 432 | PrivateKey privateKey = privateKeys[i]; 433 | X509Certificate certificate = certificates[i]; 434 | PublicKey publicKey = certificate.getPublicKey(); 435 | String keyAlgorithm = privateKey.getAlgorithm(); 436 | if (!keyAlgorithm.equalsIgnoreCase(publicKey.getAlgorithm())) { 437 | throw new InvalidKeyException( 438 | "Key algorithm of private key #" + (i + 1) + " does not match key" 439 | + " algorithm of public key #" + (i + 1) + ": " + keyAlgorithm 440 | + " vs " + publicKey.getAlgorithm()); 441 | } 442 | ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig(); 443 | signerConfig.privateKey = privateKey; 444 | signerConfig.certificates = Collections.singletonList(certificate); 445 | List signatureAlgorithms = new ArrayList<>(digestAlgorithms.length); 446 | for (String digestAlgorithm : digestAlgorithms) { 447 | try { 448 | signatureAlgorithms.add(getV2SignatureAlgorithm(keyAlgorithm, digestAlgorithm)); 449 | } catch (IllegalArgumentException e) { 450 | throw new InvalidKeyException( 451 | "Unsupported key and digest algorithm combination for signer #" 452 | + (i + 1), e); 453 | } 454 | } 455 | signerConfig.signatureAlgorithms = signatureAlgorithms; 456 | result.add(signerConfig); 457 | } 458 | return result; 459 | } 460 | 461 | private static int getV2SignatureAlgorithm(String keyAlgorithm, String digestAlgorithm) { 462 | if ("SHA-256".equalsIgnoreCase(digestAlgorithm)) { 463 | if ("RSA".equalsIgnoreCase(keyAlgorithm)) { 464 | // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee 465 | // deterministic signatures which make life easier for OTA updates (fewer files 466 | // changed when deterministic signature schemes are used). 467 | return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256; 468 | } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { 469 | return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA256; 470 | } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { 471 | return ApkSignerV2.SIGNATURE_DSA_WITH_SHA256; 472 | } else { 473 | throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); 474 | } 475 | } else if ("SHA-512".equalsIgnoreCase(digestAlgorithm)) { 476 | if ("RSA".equalsIgnoreCase(keyAlgorithm)) { 477 | // Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee 478 | // deterministic signatures which make life easier for OTA updates (fewer files 479 | // changed when deterministic signature schemes are used). 480 | return ApkSignerV2.SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512; 481 | } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { 482 | return ApkSignerV2.SIGNATURE_ECDSA_WITH_SHA512; 483 | } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { 484 | return ApkSignerV2.SIGNATURE_DSA_WITH_SHA512; 485 | } else { 486 | throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); 487 | } 488 | } else { 489 | throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm); 490 | } 491 | } 492 | 493 | public static void sign(X509Certificate cert, PrivateKey key, 494 | JarMap inputJar, OutputStream outputStream) throws Exception { 495 | int alignment = 4; 496 | int hashes = 0; 497 | 498 | X509Certificate[] publicKey = new X509Certificate[1]; 499 | publicKey[0] = cert; 500 | hashes |= getDigestAlgorithm(publicKey[0]); 501 | 502 | // Set all ZIP file timestamps to Jan 1 2009 00:00:00. 503 | long timestamp = 1230768000000L; 504 | // The Java ZipEntry API we're using converts milliseconds since epoch into MS-DOS 505 | // timestamp using the current timezone. We thus adjust the milliseconds since epoch 506 | // value to end up with MS-DOS timestamp of Jan 1 2009 00:00:00. 507 | timestamp -= TimeZone.getDefault().getOffset(timestamp); 508 | 509 | PrivateKey[] privateKey = new PrivateKey[1]; 510 | privateKey[0] = key; 511 | 512 | // Generate, in memory, an APK signed using standard JAR Signature Scheme. 513 | ByteArrayOutputStream v1SignedApkBuf = new ByteArrayOutputStream(); 514 | JarOutputStream outputJar = new JarOutputStream(v1SignedApkBuf); 515 | // Use maximum compression for compressed entries because the APK lives forever on 516 | // the system partition. 517 | outputJar.setLevel(9); 518 | Manifest manifest = addDigestsToManifest(inputJar, hashes); 519 | copyFiles(manifest, inputJar, outputJar, timestamp, alignment); 520 | signFile(manifest, publicKey, privateKey, timestamp, outputJar); 521 | outputJar.close(); 522 | ByteBuffer v1SignedApk = ByteBuffer.wrap(v1SignedApkBuf.toByteArray()); 523 | v1SignedApkBuf.reset(); 524 | 525 | ByteBuffer[] outputChunks; 526 | List signerConfigs = createV2SignerConfigs(privateKey, publicKey, 527 | new String[]{APK_SIG_SCHEME_V2_DIGEST_ALGORITHM}); 528 | outputChunks = ApkSignerV2.sign(v1SignedApk, signerConfigs); 529 | 530 | // This assumes outputChunks are array-backed. To avoid this assumption, the 531 | // code could be rewritten to use FileChannel. 532 | for (ByteBuffer outputChunk : outputChunks) { 533 | outputStream.write(outputChunk.array(), 534 | outputChunk.arrayOffset() + outputChunk.position(), outputChunk.remaining()); 535 | outputChunk.position(outputChunk.limit()); 536 | } 537 | } 538 | 539 | /** 540 | * Write to another stream and track how many bytes have been 541 | * written. 542 | */ 543 | private static class CountOutputStream extends FilterOutputStream { 544 | private int mCount; 545 | 546 | public CountOutputStream(OutputStream out) { 547 | super(out); 548 | mCount = 0; 549 | } 550 | 551 | @Override 552 | public void write(int b) throws IOException { 553 | super.write(b); 554 | mCount++; 555 | } 556 | 557 | @Override 558 | public void write(byte[] b, int off, int len) throws IOException { 559 | super.write(b, off, len); 560 | mCount += len; 561 | } 562 | 563 | public int size() { 564 | return mCount; 565 | } 566 | } 567 | } 568 | -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/hideapk/signing/ZipUtils.java: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk.signing; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.ByteOrder; 5 | 6 | /** 7 | * Assorted ZIP format helpers. 8 | * 9 | *

NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte 10 | * order of these buffers is little-endian. 11 | */ 12 | public abstract class ZipUtils { 13 | 14 | private static final int ZIP_EOCD_REC_MIN_SIZE = 22; 15 | private static final int ZIP_EOCD_REC_SIG = 0x06054b50; 16 | private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12; 17 | private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16; 18 | private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20; 19 | 20 | private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; 21 | private static final int ZIP64_EOCD_LOCATOR_SIG = 0x07064b50; 22 | 23 | private static final int UINT16_MAX_VALUE = 0xffff; 24 | 25 | private ZipUtils() { 26 | } 27 | 28 | /** 29 | * Returns the position at which ZIP End of Central Directory record starts in the provided 30 | * buffer or {@code -1} if the record is not present. 31 | * 32 | *

NOTE: Byte order of {@code zipContents} must be little-endian. 33 | */ 34 | public static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) { 35 | assertByteOrderLittleEndian(zipContents); 36 | 37 | // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive. 38 | // The record can be identified by its 4-byte signature/magic which is located at the very 39 | // beginning of the record. A complication is that the record is variable-length because of 40 | // the comment field. 41 | // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from 42 | // end of the buffer for the EOCD record signature. Whenever we find a signature, we check 43 | // the candidate record's comment length is such that the remainder of the record takes up 44 | // exactly the remaining bytes in the buffer. The search is bounded because the maximum 45 | // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number. 46 | 47 | int archiveSize = zipContents.capacity(); 48 | if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) { 49 | return -1; 50 | } 51 | int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE); 52 | int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE; 53 | for (int expectedCommentLength = 0; expectedCommentLength < maxCommentLength; expectedCommentLength++) { 54 | int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength; 55 | if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) { 56 | int actualCommentLength = getUnsignedInt16(zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET); 57 | if (actualCommentLength == expectedCommentLength) { 58 | return eocdStartPos; 59 | } 60 | } 61 | } 62 | 63 | return -1; 64 | } 65 | 66 | /** 67 | * Returns {@code true} if the provided buffer contains a ZIP64 End of Central Directory 68 | * Locator. 69 | * 70 | *

NOTE: Byte order of {@code zipContents} must be little-endian. 71 | */ 72 | public static boolean isZip64EndOfCentralDirectoryLocatorPresent(ByteBuffer zipContents, int zipEndOfCentralDirectoryPosition) { 73 | assertByteOrderLittleEndian(zipContents); 74 | 75 | // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central 76 | // Directory Record. 77 | 78 | int locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE; 79 | if (locatorPosition < 0) { 80 | return false; 81 | } 82 | 83 | return zipContents.getInt(locatorPosition) == ZIP64_EOCD_LOCATOR_SIG; 84 | } 85 | 86 | /** 87 | * Returns the offset of the start of the ZIP Central Directory in the archive. 88 | * 89 | *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 90 | */ 91 | public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) { 92 | assertByteOrderLittleEndian(zipEndOfCentralDirectory); 93 | return getUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); 94 | } 95 | 96 | /** 97 | * Sets the offset of the start of the ZIP Central Directory in the archive. 98 | * 99 | *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 100 | */ 101 | public static void setZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory, long offset) { 102 | assertByteOrderLittleEndian(zipEndOfCentralDirectory); 103 | setUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET, offset); 104 | } 105 | 106 | /** 107 | * Returns the size (in bytes) of the ZIP Central Directory. 108 | * 109 | *

NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. 110 | */ 111 | public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) { 112 | assertByteOrderLittleEndian(zipEndOfCentralDirectory); 113 | return getUnsignedInt32(zipEndOfCentralDirectory, zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET); 114 | } 115 | 116 | private static void assertByteOrderLittleEndian(ByteBuffer buffer) { 117 | if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { 118 | throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); 119 | } 120 | } 121 | 122 | private static int getUnsignedInt16(ByteBuffer buffer, int offset) { 123 | return buffer.getShort(offset) & 0xffff; 124 | } 125 | 126 | private static long getUnsignedInt32(ByteBuffer buffer, int offset) { 127 | return buffer.getInt(offset) & 0xffffffffL; 128 | } 129 | 130 | private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) { 131 | if ((value < 0) || (value > 0xffffffffL)) { 132 | throw new IllegalArgumentException("uint32 value of out range: " + value); 133 | } 134 | buffer.putInt(buffer.position() + offset, (int) value); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/hideapk/utils/AXML.kt: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk.utils 2 | 3 | import java.io.ByteArrayOutputStream 4 | import java.nio.ByteBuffer 5 | import java.nio.ByteOrder.LITTLE_ENDIAN 6 | import java.nio.charset.Charset 7 | import java.util.* 8 | 9 | class AXML(b: ByteArray) { 10 | 11 | var bytes = b 12 | private set 13 | 14 | companion object { 15 | private const val CHUNK_SIZE_OFF = 4 16 | private const val STRING_INDICES_OFF = 7 * 4 17 | private val UTF_16LE = Charset.forName("UTF-16LE") 18 | } 19 | 20 | /** 21 | * String pool header: 22 | * 0: 0x1C0001 23 | * 1: chunk size 24 | * 2: number of strings 25 | * 3: number of styles (assert as 0) 26 | * 4: flags 27 | * 5: offset to string data 28 | * 6: offset to style data (assert as 0) 29 | * 30 | * Followed by an array of uint32_t with size = number of strings 31 | * Each entry points to an offset into the string data 32 | */ 33 | fun findAndPatch(vararg patterns: Pair): Boolean { 34 | val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN) 35 | 36 | fun findStringPool(): Int { 37 | var offset = 8 38 | while (offset < bytes.size) { 39 | if (buffer.getInt(offset) == 0x1C0001) 40 | return offset 41 | offset += buffer.getInt(offset + CHUNK_SIZE_OFF) 42 | } 43 | return -1 44 | } 45 | 46 | var patch = false 47 | val start = findStringPool() 48 | if (start < 0) 49 | return false 50 | 51 | // Read header 52 | buffer.position(start + 4) 53 | val intBuf = buffer.asIntBuffer() 54 | val size = intBuf.get() 55 | val count = intBuf.get() 56 | intBuf.get() 57 | intBuf.get() 58 | val dataOff = start + intBuf.get() 59 | intBuf.get() 60 | 61 | val strings = ArrayList(count) 62 | // Read and patch all strings 63 | loop@ for (i in 0 until count) { 64 | val off = dataOff + intBuf.get() 65 | val len = buffer.getShort(off) 66 | val str = String(bytes, off + 2, len * 2, UTF_16LE) 67 | for ((from, to) in patterns) { 68 | if (str.contains(from)) { 69 | strings.add(str.replace(from, to)) 70 | patch = true 71 | continue@loop 72 | } 73 | } 74 | strings.add(str) 75 | } 76 | 77 | if (!patch) 78 | return false 79 | 80 | // Write everything before string data, will patch values later 81 | val baos = RawByteStream() 82 | baos.write(bytes, 0, dataOff) 83 | 84 | // Write string data 85 | val strList = IntArray(count) 86 | for (i in 0 until count) { 87 | strList[i] = baos.size() - dataOff 88 | val str = strings[i] 89 | baos.write(str.length.toShortBytes()) 90 | baos.write(str.toByteArray(UTF_16LE)) 91 | // Null terminate 92 | baos.write(0) 93 | baos.write(0) 94 | } 95 | baos.align() 96 | 97 | val sizeDiff = baos.size() - start - size 98 | val newBuffer = ByteBuffer.wrap(baos.buf).order(LITTLE_ENDIAN) 99 | 100 | // Patch XML size 101 | newBuffer.putInt(CHUNK_SIZE_OFF, buffer.getInt(CHUNK_SIZE_OFF) + sizeDiff) 102 | // Patch string pool size 103 | newBuffer.putInt(start + CHUNK_SIZE_OFF, size + sizeDiff) 104 | // Patch index table 105 | newBuffer.position(start + STRING_INDICES_OFF) 106 | val newIntBuf = newBuffer.asIntBuffer() 107 | strList.forEach { newIntBuf.put(it) } 108 | 109 | // Write the rest of the chunks 110 | val nextOff = start + size 111 | baos.write(bytes, nextOff, bytes.size - nextOff) 112 | 113 | bytes = baos.toByteArray() 114 | return true 115 | } 116 | 117 | private fun Int.toShortBytes(): ByteArray { 118 | val b = ByteBuffer.allocate(2).order(LITTLE_ENDIAN) 119 | b.putShort(this.toShort()) 120 | return b.array() 121 | } 122 | 123 | private class RawByteStream : ByteArrayOutputStream() { 124 | val buf: ByteArray get() = buf 125 | 126 | fun align(alignment: Int = 4) { 127 | val newCount = (count + alignment - 1) / alignment * alignment 128 | for (i in 0 until (newCount - count)) 129 | write(0) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/hideapk/utils/Keygen.kt: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk.utils 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageManager 5 | import android.util.Base64 6 | import android.util.Base64OutputStream 7 | import com.shocker.hideapk.signing.CryptoUtils.readCertificate 8 | import com.shocker.hideapk.signing.CryptoUtils.readPrivateKey 9 | import org.bouncycastle.asn1.x500.X500Name 10 | import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter 11 | import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder 12 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder 13 | import java.io.ByteArrayInputStream 14 | import java.io.ByteArrayOutputStream 15 | import java.math.BigInteger 16 | import java.security.KeyPairGenerator 17 | import java.security.KeyStore 18 | import java.security.MessageDigest 19 | import java.security.PrivateKey 20 | import java.security.cert.X509Certificate 21 | import java.util.* 22 | import java.util.zip.GZIPInputStream 23 | import java.util.zip.GZIPOutputStream 24 | 25 | private interface CertKeyProvider { 26 | val cert: X509Certificate 27 | val key: PrivateKey 28 | } 29 | 30 | @Suppress("DEPRECATION") 31 | class Keygen(context: Context) : CertKeyProvider { 32 | 33 | companion object { 34 | private const val ALIAS = "magisk" 35 | private val PASSWORD get() = "magisk".toCharArray() 36 | private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81" 37 | private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android" 38 | private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP 39 | } 40 | 41 | private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) } 42 | private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) } 43 | 44 | override val cert get() = provider.cert 45 | override val key get() = provider.key 46 | 47 | private val provider: CertKeyProvider 48 | 49 | inner class KeyStoreProvider : 50 | CertKeyProvider { 51 | private val ks by lazy { init() } 52 | override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate } 53 | override val key by lazy { ks.getKey( 54 | ALIAS, 55 | PASSWORD 56 | ) as PrivateKey } 57 | } 58 | 59 | 60 | init { 61 | val pm = context.packageManager 62 | val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES) 63 | val sig = info.signatures[0] 64 | val digest = MessageDigest.getInstance("SHA1") 65 | val chksum = digest.digest(sig.toByteArray()) 66 | 67 | val sb = StringBuilder() 68 | for (b in chksum) { 69 | sb.append("%02x".format(0xFF and b.toInt())) 70 | } 71 | provider = KeyStoreProvider() 72 | } 73 | 74 | private fun init(): KeyStore { 75 | val ks = KeyStore.getInstance("PKCS12") 76 | ks.load(null) 77 | 78 | // Keys already exist 79 | if (ks.containsAlias(ALIAS)) 80 | return ks 81 | 82 | // Generate new private key and certificate 83 | val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair() 84 | val dname = X500Name(DNAME) 85 | val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()), 86 | start.time, end.time, dname, kp.public) 87 | val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private) 88 | val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer)) 89 | 90 | // Store them into keystore 91 | ks.setKeyEntry( 92 | ALIAS, kp.private, 93 | PASSWORD, arrayOf(cert)) 94 | val bytes = ByteArrayOutputStream() 95 | GZIPOutputStream(Base64OutputStream(bytes, 96 | BASE64_FLAG 97 | )).use { 98 | ks.store(it, 99 | PASSWORD 100 | ) 101 | } 102 | // Config.keyStoreRaw = bytes.toString("UTF-8") 103 | 104 | return ks 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/shocker/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.shocker.ui 2 | 3 | import android.app.Activity 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.os.Bundle 6 | import com.shocker.hideapk.databinding.ActivityMainBinding 7 | import com.shocker.hideapk.hide.HideAPK 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.launch 10 | 11 | class MainActivity : AppCompatActivity() { 12 | 13 | private lateinit var binding: ActivityMainBinding 14 | private lateinit var activity: Activity 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | 19 | 20 | binding = ActivityMainBinding.inflate(layoutInflater) 21 | setContentView(binding.root) 22 | 23 | // Example of a call to a native method 24 | binding.sampleText.text = stringFromJNI() 25 | 26 | activity=this 27 | GlobalScope.launch { 28 | HideAPK.hide(activity,"randomName",getApplicationInfo().sourceDir) 29 | } 30 | } 31 | 32 | /** 33 | * A native method that is implemented by the 'hideapk' native library, 34 | * which is packaged with this application. 35 | */ 36 | external fun stringFromJNI(): String 37 | 38 | companion object { 39 | // Used to load the 'hideapk' library on application startup. 40 | init { 41 | System.loadLibrary("hideapk") 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | HideAPK 3 | 4 | 5 | 模块 6 | 超级用户 7 | 日志 8 | 设置 9 | 安装 10 | 主页 11 | 主题 12 | 排除列表 13 | 14 | 15 | 无法连接 16 | 更新日志 17 | 正在加载 18 | 更新 19 | 无法获取 20 | 不再显示 21 | 包名 22 | App 23 | 24 | 仅从官方 GitHub 页面下载 Magisk。未知来源的文件可能具有恶意行为! 25 | 支持开发 26 | 源代码 27 | Magisk 将一直保持免费且开源,向开发者捐赠以表示支持。 28 | 当前 29 | 最新 30 | 无效的更新通道 31 | 卸载 Magisk 32 | 所有模块将被停用或删除!超级用户权限丢失!\n如果设备尚未加密,用户数据可能被自动加密。 33 | 34 | 35 | 保持强制加密 36 | 保留 AVB 2.0/dm-verity 37 | 修补 boot 镜像中的 vbmeta 38 | 安装到 Recovery 39 | 选项 40 | 方式 41 | 下一步 42 | 开始 43 | 立即安装 44 | 直接安装(推荐) 45 | 安装到未使用的槽位(OTA 后) 46 | 将在重启后强制切换到另一个槽位!注意只能在 OTA 更新完成后的重启之前使用。 47 | 修复安装 48 | 选择并修补一个文件 49 | 选择一个原始映像文件(*.img)或一个 Odin 包(*.tar) 50 | 设备将在 5 秒后重启 51 | 安装 52 | 53 | 54 | 超级用户请求 55 | 由于某个应用遮挡了超级用户请求界面,因此 Magisk 无法验证您的回应 56 | 拒绝 57 | 提示 58 | 允许 59 | 将授予对该设备的最高权限。\n如果不确定,请拒绝! 60 | 永久 61 | 仅此一次 62 | 10 分钟 63 | 20 分钟 64 | 30 分钟 65 | 60 分钟 66 | %1$s 已被授予超级用户权限 67 | %1$s 已被拒绝超级用户权限 68 | 已授予 %1$s 超级用户权限 69 | 已拒绝 %1$s 超级用户权限 70 | 已启用 %1$s 的通知 71 | 已禁用 %1$s 的通知 72 | 已启用对 %1$s 的日志记录 73 | 已禁用对 %1$s 的日志记录 74 | 撤销 75 | 确认撤销授予 %1$s 的权限? 76 | 消息提示 77 | 78 | 79 | 通知 80 | 撤销 81 | 尚无应用请求超级用户权限 82 | 83 | 84 | 没有超级用户权限使用日志 85 | 没有 Magisk 日志 86 | 保存日志 87 | 清空日志 88 | 日志已清空 89 | PID: %1$d 90 | 目标 UID: %1$d 91 | 92 | 93 | 94 | 95 | 显示系统应用 96 | 显示操作系统 97 | 按名称过滤 98 | 搜索 99 | 100 | 101 | (未提供信息) 102 | 软重启 103 | 重启到 Recovery 104 | 重启到 Bootloader 105 | 重启到 Download 106 | 重启到 EDL 107 | %1$s,作者 %2$s 108 | 移除 109 | 还原 110 | 从本地安装 111 | 可更新 112 | %1$s 已启用,此模块暂停加载 113 | %1$s 未启用,此模块暂停加载 114 | 存在兼容性问题,此模块未加载 115 | 116 | 117 | 主题模式 118 | 选择一个模式 119 | 明亮模式 120 | 跟随系统 121 | 深色模式 122 | 下载路径 123 | 文件将保存到 %1$s 124 | 隐藏 Magisk 应用 125 | 安装具有随机包名和自定义应用名称的代理应用 126 | 还原 Magisk 应用 127 | 取消隐藏,恢复到原始应用 128 | 语言 129 | 系统默认 130 | 检查更新 131 | 定期在后台检查更新 132 | 更新通道 133 | 稳定版 134 | 测试版 135 | 自定义通道 136 | 自定义通道网址 137 | 在 Zygote 中运行 Magisk 138 | 遵守排除列表 139 | Magisk 不会修改列表中的进程 140 | 此功能需要先启用 %1$s 141 | 配置排除列表 142 | 选择加入排除列表的进程 143 | Systemless hosts 144 | 为广告屏蔽应用提供 Systemless hosts 支持 145 | 已添加 systemless hosts 模块 146 | 新的应用名称 147 | 将使用新名称重新安装本应用 148 | 无效输入 149 | 应用和 ADB 150 | 仅应用 151 | 仅 ADB 152 | 已禁用 153 | 10 秒 154 | 15 秒 155 | 20 秒 156 | 30 秒 157 | 45 秒 158 | 60 秒 159 | 超级用户访问权限 160 | 自动响应 161 | 请求超时 162 | 超级用户通知 163 | 更新后重新认证 164 | 应用更新后重新认证超级用户权限 165 | 点按劫持保护 166 | 存在屏幕叠加层时,超级用户请求弹窗不响应允许操作 167 | 生物识别验证 168 | 使用生物识别来允许超级用户请求 169 | 设备不支持或未配置生物识别功能 170 | 个性化 171 | 在隐藏后难以识别名称和图标的情况下,添加快捷方式到桌面 172 | 安全 DNS(DoH) 173 | 解决某些地区的 DNS 污染问题 174 | 175 | 多用户模式 176 | 仅设备所有者 177 | 由设备所有者管理 178 | 各用户独立 179 | 仅设备所有者有超级用户权限 180 | 仅设备所有者能管理超级用户并接收权限请求提示 181 | 每个用户有独立的超级用户规则 182 | 183 | 挂载命名空间模式 184 | 全局命名空间 185 | 继承命名空间 186 | 独立命名空间 187 | 所有 ROOT 会话使用全局挂载命名空间 188 | ROOT 会话继承原进程的命名空间 189 | 每个 ROOT 会话使用独立的命名空间 190 | 191 | 192 | 更新提示 193 | 下载进度 194 | 更新完成 195 | 下载完成 196 | 下载失败 197 | Magisk 已发布新版本! 198 | Magisk 已完成更新 199 | 点按即可打开应用 200 | 201 | 202 | 203 | 204 | 安装 %1$s %2$s(%3$d) 205 | 下载 206 | 重启 207 | 发布说明 208 | 正在刷入 209 | 完成! 210 | 失败 211 | 正在隐藏 Magisk 应用 212 | 找不到能打开此链接的应用 213 | 完全卸载 214 | 还原原厂映像 215 | 正在还原 216 | 已还原 217 | 原厂 Boot 映像的备份不存在 218 | 安装失败 219 | 需要修复运行环境 220 | 需要一些额外的安装才能使 Magisk 正常工作。完成后自动重启,是否继续? 221 | 正在修复运行环境 222 | 验证您的身份 223 | 不支持的 Magisk 版本 224 | 应用不支持低于 %1$s 版本的 Magisk,表现为未安装状态。但升级功能可用,请尽快在应用内升级 Magisk。 225 | 异常状态 226 | Magisk 不支持安装为系统应用,请还原为用户应用。 227 | 检测到不属于 Magisk 的 su 文件,请删除其他超级用户程序。 228 | 不支持将 Magisk 安装到外置存储卡,请将应用移动回内部存储空间。 229 | 超级用户权限丢失,应用无法在隐藏状态下继续工作,请恢复到原始 Magisk 应用。 230 | @string/settings_restore_app_title 231 | 允许访问存储空间以使用此功能 232 | 允许安装未知应用以使用此功能 233 | 添加快捷方式到桌面 234 | 隐藏后应用的名字和图标可能难以识别。需要在桌面上添加具有原始名称和图标的快捷方式吗? 235 | 找不到可处理此操作的应用 236 | 重启后生效 237 | 即将把隐藏的 Magisk 应用恢复回原始应用,是否继续? 238 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/com/shocker/hideapk/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.shocker.hideapk 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | plugins { 3 | id 'com.android.application' version '7.1.2' apply false 4 | id 'com.android.library' version '7.1.2' apply false 5 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false 6 | } 7 | 8 | 9 | ext { 10 | targetCompatibility = JavaVersion.VERSION_11 11 | sourceCompatibility = JavaVersion.VERSION_11 12 | targetCompatibility1 = JavaVersion.VERSION_11 13 | }// Top-level build file where you can add configuration options common to all sub-projects/modules. 14 | 15 | task clean(type: Delete) { 16 | delete rootProject.buildDir 17 | } -------------------------------------------------------------------------------- /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. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec: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 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PShocker/HideAPK/bedcf3692c9d9afdfb8dec6c00a61c894cdd9688/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jun 25 13:28:04 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { url 'https://jitpack.io' } 14 | } 15 | } 16 | rootProject.name = "HideAPK" 17 | include ':app' 18 | --------------------------------------------------------------------------------