Add NestedZipLink to shrink apk size (V2 signing crashes)
This commit is contained in:
parent
2d0a2bdd27
commit
b44b06f268
|
|
@ -14,12 +14,10 @@ dependencies {
|
|||
implementation project(':axmlprinter')
|
||||
implementation project(':share')
|
||||
implementation 'commons-io:commons-io:2.10.0'
|
||||
implementation 'com.android.tools.build:apkzlib:4.2.1'
|
||||
implementation 'com.android.tools.build:apkzlib:4.2.2'
|
||||
implementation 'com.beust:jcommander:1.81'
|
||||
}
|
||||
|
||||
sourceSets.main.java.srcDirs += "$rootProject.projectDir/apksigner/src/apksigner/java"
|
||||
|
||||
jar {
|
||||
baseName = "lspatch"
|
||||
destinationDirectory = new File("$rootProject.projectDir/out")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package org.lsposed.patch;
|
||||
|
||||
import com.android.apksig.ApkSigner;
|
||||
import com.android.tools.build.apkzlib.sign.SigningExtension;
|
||||
import com.android.tools.build.apkzlib.sign.SigningOptions;
|
||||
import com.android.tools.build.apkzlib.zip.AlignmentRules;
|
||||
import com.android.tools.build.apkzlib.zip.StoredEntry;
|
||||
import com.android.tools.build.apkzlib.zip.ZFile;
|
||||
|
|
@ -17,6 +18,8 @@ import org.apache.commons.io.FilenameUtils;
|
|||
import org.lsposed.lspatch.share.Constants;
|
||||
import org.lsposed.patch.util.ApkSignatureHelper;
|
||||
import org.lsposed.patch.util.ManifestParser;
|
||||
import org.lsposed.patch.util.NestedZipLink;
|
||||
import org.lsposed.patch.util.NestedZipLink.NestedZip;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
|
@ -87,7 +90,6 @@ public class LSPatch {
|
|||
private static final String SIGNATURE_INFO_ASSET_PATH = "assets/original_signature_info.ini";
|
||||
private static final String ORIGINAL_APK_ASSET_PATH = "assets/origin_apk.bin";
|
||||
private static final String ANDROID_MANIFEST_XML = "AndroidManifest.xml";
|
||||
private static final String RESOURCES_ARSC = "resources.arsc";
|
||||
private static final HashSet<String> APK_LIB_PATH_ARRAY = new HashSet<>(Arrays.asList(
|
||||
// "armeabi",
|
||||
"armeabi-v7a",
|
||||
|
|
@ -98,7 +100,6 @@ public class LSPatch {
|
|||
|
||||
private static final ZFileOptions Z_FILE_OPTIONS = new ZFileOptions().setAlignmentRule(AlignmentRules.compose(
|
||||
AlignmentRules.constantForSuffix(".so", 4096),
|
||||
AlignmentRules.constantForSuffix(RESOURCES_ARSC, 4),
|
||||
AlignmentRules.constantForSuffix(ORIGINAL_APK_ASSET_PATH, 4096)
|
||||
));
|
||||
|
||||
|
|
@ -149,25 +150,16 @@ public class LSPatch {
|
|||
throw new PatchError("The source apk file does not exit. Please provide a correct path.");
|
||||
|
||||
File tmpApk = Files.createTempFile(srcApkFile.getName(), "unsigned").toFile();
|
||||
tmpApk.delete();
|
||||
|
||||
if (verbose)
|
||||
System.out.println("apk path: " + srcApkFile);
|
||||
|
||||
System.out.println("Copying to tmp apk...");
|
||||
|
||||
FileUtils.copyFile(srcApkFile, tmpApk);
|
||||
|
||||
System.out.println("Parsing original apk...");
|
||||
|
||||
try (ZFile zFile = ZFile.openReadWrite(tmpApk, Z_FILE_OPTIONS)) {
|
||||
try (ZFile srcZFile = ZFile.openReadOnly(srcApkFile); ZFile dstZFile = ZFile.openReadWrite(tmpApk, Z_FILE_OPTIONS)) {
|
||||
// copy origin apk to assets
|
||||
zFile.add(ORIGINAL_APK_ASSET_PATH, new FileInputStream(srcApkFile), false);
|
||||
|
||||
// remove unnecessary files
|
||||
for (StoredEntry storedEntry : zFile.entries()) {
|
||||
var name = storedEntry.getCentralDirectoryHeader().getName();
|
||||
if (name.endsWith(".dex")) storedEntry.delete();
|
||||
}
|
||||
dstZFile.add(ORIGINAL_APK_ASSET_PATH, new FileInputStream(srcApkFile), false);
|
||||
|
||||
if (sigbypassLevel > 0) {
|
||||
// save the apk original signature info, to support crack signature.
|
||||
|
|
@ -178,14 +170,14 @@ public class LSPatch {
|
|||
if (verbose)
|
||||
System.out.println("Original signature\n" + originalSignature);
|
||||
try (var is = new ByteArrayInputStream(originalSignature.getBytes(StandardCharsets.UTF_8))) {
|
||||
zFile.add(SIGNATURE_INFO_ASSET_PATH, is);
|
||||
dstZFile.add(SIGNATURE_INFO_ASSET_PATH, is);
|
||||
} catch (Throwable e) {
|
||||
throw new PatchError("Error when saving signature: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
// copy out manifest file from zlib
|
||||
var manifestEntry = zFile.get(ANDROID_MANIFEST_XML);
|
||||
var manifestEntry = srcZFile.get(ANDROID_MANIFEST_XML);
|
||||
if (manifestEntry == null)
|
||||
throw new PatchError("Provided file is not a valid apk");
|
||||
|
||||
|
|
@ -204,21 +196,21 @@ public class LSPatch {
|
|||
System.out.println("Patching apk...");
|
||||
// modify manifest
|
||||
try (var is = new ByteArrayInputStream(modifyManifestFile(manifestEntry.open()))) {
|
||||
zFile.add(ANDROID_MANIFEST_XML, is);
|
||||
dstZFile.add(ANDROID_MANIFEST_XML, is);
|
||||
} catch (Throwable e) {
|
||||
throw new PatchError("Error when modifying manifest: " + e);
|
||||
}
|
||||
|
||||
// save original appComponentFactory name to asset file even its empty
|
||||
try (var is = new ByteArrayInputStream(appComponentFactory.getBytes(StandardCharsets.UTF_8))) {
|
||||
zFile.add(APP_COMPONENT_FACTORY_ASSET_PATH, is);
|
||||
dstZFile.add(APP_COMPONENT_FACTORY_ASSET_PATH, is);
|
||||
} catch (Throwable e) {
|
||||
throw new PatchError("Error when saving appComponentFactory class: " + e);
|
||||
}
|
||||
|
||||
// save original main application name to asset file even its empty
|
||||
try (var is = new ByteArrayInputStream(applicationName.getBytes(StandardCharsets.UTF_8))) {
|
||||
zFile.add(APPLICATION_NAME_ASSET_PATH, is);
|
||||
dstZFile.add(APPLICATION_NAME_ASSET_PATH, is);
|
||||
} catch (Throwable e) {
|
||||
throw new PatchError("Error when saving application name: " + e);
|
||||
}
|
||||
|
|
@ -231,7 +223,7 @@ public class LSPatch {
|
|||
for (String arch : APK_LIB_PATH_ARRAY) {
|
||||
String entryName = "assets/lib/" + arch + "/liblspd.so";
|
||||
try (var is = getClass().getClassLoader().getResourceAsStream("assets/so/" + (arch.equals("armeabi") ? "armeabi-v7a" : arch) + "/liblspd.so")) {
|
||||
zFile.add(entryName, is, false); // no compress for so
|
||||
dstZFile.add(entryName, is, false); // no compress for so
|
||||
} catch (Throwable e) {
|
||||
// More exception info
|
||||
throw new PatchError("Error when adding native lib", e);
|
||||
|
|
@ -244,35 +236,66 @@ public class LSPatch {
|
|||
System.out.println("Adding dex..");
|
||||
|
||||
try (var is = getClass().getClassLoader().getResourceAsStream("assets/dex/loader.dex")) {
|
||||
zFile.add("classes.dex", is);
|
||||
dstZFile.add("classes.dex", is);
|
||||
} catch (Throwable e) {
|
||||
throw new PatchError("Error when add dex: " + e);
|
||||
}
|
||||
|
||||
try (var is = getClass().getClassLoader().getResourceAsStream("assets/dex/lsp.dex")) {
|
||||
zFile.add("assets/lsp", is);
|
||||
dstZFile.add("assets/lsp", is);
|
||||
} catch (Throwable e) {
|
||||
throw new PatchError("Error when add assets: " + e);
|
||||
}
|
||||
|
||||
// save lspatch config to asset..
|
||||
try (var is = new ByteArrayInputStream("42".getBytes(StandardCharsets.UTF_8))) {
|
||||
zFile.add("assets/" + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel, is);
|
||||
dstZFile.add("assets/" + Constants.CONFIG_NAME_SIGBYPASSLV + sigbypassLevel, is);
|
||||
} catch (Throwable e) {
|
||||
throw new PatchError("Error when saving signature bypass level: " + e);
|
||||
}
|
||||
|
||||
embedModules(zFile);
|
||||
embedModules(dstZFile);
|
||||
|
||||
dstZFile.realign();
|
||||
|
||||
// sign apk
|
||||
System.out.println("Signing apk...");
|
||||
var sign = zFile.get("META-INF/MANIFEST.MF");
|
||||
if (sign != null)
|
||||
sign.delete();
|
||||
try {
|
||||
var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
try (var is = getClass().getClassLoader().getResourceAsStream("assets/keystore")) {
|
||||
keyStore.load(is, "123456".toCharArray());
|
||||
}
|
||||
var entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry("key0", new KeyStore.PasswordProtection("123456".toCharArray()));
|
||||
new SigningExtension(SigningOptions.builder()
|
||||
.setMinSdkVersion(27)
|
||||
.setV1SigningEnabled(v1)
|
||||
// Don't know why it crashes !!
|
||||
.setV2SigningEnabled(v2)
|
||||
.setCertificates((X509Certificate[]) entry.getCertificateChain())
|
||||
.setKey(entry.getPrivateKey())
|
||||
.build()).register(dstZFile);
|
||||
} catch (Exception e) {
|
||||
throw new PatchError("Failed to sign apk: " + e.getMessage());
|
||||
}
|
||||
|
||||
zFile.realign();
|
||||
zFile.update();
|
||||
// create zip link
|
||||
if (verbose)
|
||||
System.out.println("Creating nested apk link...");
|
||||
|
||||
signApkUsingAndroidApksigner(tmpApk, outputFile);
|
||||
NestedZipLink nestedZipLink = new NestedZipLink(dstZFile);
|
||||
StoredEntry originalZipEntry = dstZFile.get(ORIGINAL_APK_ASSET_PATH);
|
||||
NestedZip nestedZip = new NestedZip(srcZFile, originalZipEntry);
|
||||
for (StoredEntry entry : srcZFile.entries()) {
|
||||
String name = entry.getCentralDirectoryHeader().getName();
|
||||
if (name.startsWith("classes") && name.endsWith(".dex")) continue;
|
||||
if (name.equals("AndroidManifest.xml")) continue;
|
||||
nestedZip.addFileLink(name);
|
||||
}
|
||||
nestedZipLink.nestedZips.add(nestedZip);
|
||||
dstZFile.addZFileExtension(nestedZipLink);
|
||||
|
||||
dstZFile.update();
|
||||
FileUtils.copyFile(tmpApk, outputFile);
|
||||
|
||||
System.out.println("Done. Output APK: " + outputFile.getAbsolutePath());
|
||||
} finally {
|
||||
|
|
@ -337,32 +360,4 @@ public class LSPatch {
|
|||
os.close();
|
||||
return os.toByteArray();
|
||||
}
|
||||
|
||||
private void signApkUsingAndroidApksigner(File apkPath, File outputPath) throws PatchError {
|
||||
try {
|
||||
var keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
try (var is = getClass().getClassLoader().getResourceAsStream("assets/keystore")) {
|
||||
keyStore.load(is, "123456".toCharArray());
|
||||
}
|
||||
var entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry("key0", new KeyStore.PasswordProtection("123456".toCharArray()));
|
||||
|
||||
ApkSigner.SignerConfig signerConfig =
|
||||
new ApkSigner.SignerConfig.Builder(
|
||||
"lspatch", entry.getPrivateKey(), Arrays.asList((X509Certificate[]) entry.getCertificateChain()))
|
||||
.build();
|
||||
ApkSigner apkSigner = new ApkSigner.Builder(List.of(signerConfig))
|
||||
.setInputApk(apkPath)
|
||||
.setOutputApk(outputPath)
|
||||
.setOtherSignersSignaturesPreserved(false)
|
||||
.setV1SigningEnabled(v1)
|
||||
.setV2SigningEnabled(v2)
|
||||
.setV3SigningEnabled(v3)
|
||||
.setDebuggableApkPermitted(true)
|
||||
.setSigningCertificateLineage(null)
|
||||
.setMinSdkVersion(27).build();
|
||||
apkSigner.sign();
|
||||
} catch (Exception e) {
|
||||
throw new PatchError("Failed to sign apk: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
package org.lsposed.patch.util;
|
||||
|
||||
import com.android.tools.build.apkzlib.utils.IOExceptionRunnable;
|
||||
import com.android.tools.build.apkzlib.zip.CentralDirectoryHeader;
|
||||
import com.android.tools.build.apkzlib.zip.StoredEntry;
|
||||
import com.android.tools.build.apkzlib.zip.ZFile;
|
||||
import com.android.tools.build.apkzlib.zip.ZFileExtension;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class NestedZipLink extends ZFileExtension {
|
||||
public static class NestedZip {
|
||||
final Set<String> links;
|
||||
final ZFile zip;
|
||||
final StoredEntry entry;
|
||||
|
||||
public NestedZip(ZFile zip, StoredEntry entry) {
|
||||
this.zip = zip;
|
||||
this.entry = entry;
|
||||
this.links = new HashSet<>();
|
||||
}
|
||||
|
||||
public void addFileLink(String name) {
|
||||
links.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
public final ZFile zFile;
|
||||
|
||||
public final Set<NestedZip> nestedZips = new HashSet<>();
|
||||
|
||||
private boolean written;
|
||||
|
||||
public NestedZipLink(ZFile zFile) {
|
||||
this.zFile = zFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IOExceptionRunnable beforeUpdate() {
|
||||
written = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entriesWritten() throws IOException {
|
||||
if (written) return;
|
||||
try {
|
||||
Method deleteDirectoryAndEocd = ZFile.class.getDeclaredMethod("deleteDirectoryAndEocd");
|
||||
deleteDirectoryAndEocd.setAccessible(true);
|
||||
deleteDirectoryAndEocd.invoke(zFile);
|
||||
appendEntries();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
var ex = new IOException("Error when writing link entries");
|
||||
ex.addSuppressed(e);
|
||||
throw ex;
|
||||
}
|
||||
written = true;
|
||||
}
|
||||
|
||||
private void appendEntries() throws IOException {
|
||||
for (var nestedZip : nestedZips) {
|
||||
long nestedZipOffset = nestedZip.entry.getCentralDirectoryHeader().getOffset();
|
||||
for (var link : nestedZip.links) {
|
||||
var entry = nestedZip.zip.get(link);
|
||||
if (entry == null) throw new IOException("Entry " + link + " does not exist in nested zip");
|
||||
CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader();
|
||||
CentralDirectoryHeader clonedCdh;
|
||||
|
||||
try {
|
||||
Method clone = CentralDirectoryHeader.class.getDeclaredMethod("clone");
|
||||
clone.setAccessible(true);
|
||||
clonedCdh = (CentralDirectoryHeader) clone.invoke(cdh);
|
||||
|
||||
zFile.add(link, new ByteArrayInputStream(new byte[0]));
|
||||
StoredEntry newEntry = zFile.get(link);
|
||||
|
||||
Field field_file = CentralDirectoryHeader.class.getDeclaredField("file");
|
||||
field_file.setAccessible(true);
|
||||
field_file.set(clonedCdh, zFile);
|
||||
|
||||
Field field_offset = CentralDirectoryHeader.class.getDeclaredField("offset");
|
||||
field_offset.setAccessible(true);
|
||||
|
||||
field_offset.set(clonedCdh, nestedZipOffset + cdh.getOffset() + nestedZip.entry.getLocalHeaderSize());
|
||||
|
||||
Field field_cdh = StoredEntry.class.getDeclaredField("cdh");
|
||||
field_cdh.setAccessible(true);
|
||||
field_cdh.set(newEntry, clonedCdh);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue