├── .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 | *