com.tencent.mm.androlib.ResourceApkBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of AndResGuard-core Show documentation
Show all versions of AndResGuard-core Show documentation
proguard resource for Android by wechat team. 迁移到Gradle 8+
The newest version!
package com.tencent.mm.androlib;
import com.tencent.mm.androlib.res.decoder.ARSCDecoder;
import com.tencent.mm.resourceproguard.Configuration;
import com.tencent.mm.resourceproguard.InputParam;
import com.tencent.mm.util.FileOperation;
import com.tencent.mm.util.TypedValue;
import com.tencent.mm.util.Utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.security.Key;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import apksigner.ApkSignerTool;
import static com.tencent.mm.resourceproguard.InputParam.SignatureType.SchemaV3;
/**
* @author shwenzhang
* modified:
* @author jonychina162
* 为了使用v2签名,引入了google v2sign 模块
* 由于使用v2签名,会对整个包除了签名块验证完整性,即除了签名块的内容在签名之后包其他内容不允许再改动,因此修改了原有的签名逻辑,
* 现有逻辑:1 zipalign 2.sign 。具体请参考buildApkV2sign
*/
public class ResourceApkBuilder {
private final Configuration config;
private File mOutDir;
private File m7zipOutPutDir;
private File mUnSignedApk;
private File mSignedApk;
private File mSignedWith7ZipApk;
private File m7ZipApk;
private File mAlignedApk;
private File mAlignedWith7ZipApk;
private String mApkName;
private File finalApkFile;
public ResourceApkBuilder(Configuration config) {
this.config = config;
}
public void setOutDir(File outDir, String apkName, File finalApkFile) {
this.mOutDir = outDir;
this.mApkName = apkName;
this.finalApkFile = finalApkFile;
}
public void buildApkWithV1sign(HashMap compressData) throws IOException, InterruptedException {
insureFileNameV1();
generalUnsignApk(compressData);
signApkV1(mUnSignedApk, mSignedApk);
use7zApk(compressData, mSignedApk, mSignedWith7ZipApk);
alignApks();
copyFinalApkV1();
}
private void copyFinalApkV1() throws IOException {
if (finalApkFile != null) {
System.out.printf("output: %s%n", finalApkFile);
if (mSignedWith7ZipApk.exists()) {
FileOperation.copyFileUsingStream(mAlignedWith7ZipApk, finalApkFile);
} else if (mSignedApk.exists()) {
FileOperation.copyFileUsingStream(mAlignedApk, finalApkFile);
}
}
}
public void buildApkWithV2V3Sign(HashMap compressData, int minSDKVersion, InputParam.SignatureType signatureType) throws Exception {
insureFileNameV2();
generalUnsignApk(compressData);
if (use7zApk(compressData, mUnSignedApk, m7ZipApk)) {
alignApk(m7ZipApk, mAlignedApk);
} else {
alignApk(mUnSignedApk, mAlignedApk);
}
/*
* Caution: If you sign your app using APK Signature Scheme v2 and make further changes to the app,
* the app's signature is invalidated.
* For this reason, use tools such as zipalign before signing your app using APK Signature Scheme v2, not after.
**/
signApkV2V3(mAlignedApk, mSignedApk, minSDKVersion, signatureType);
copyFinalApkV2();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private void copyFinalApkV2() throws IOException {
if (mSignedApk.exists() && finalApkFile != null) {
System.out.printf("output: %s%n", finalApkFile);
FileOperation.copyFileUsingStream(mSignedApk, finalApkFile);
File v4IdFile = new File(mAlignedApk.getAbsolutePath() + ".idsig");
if (v4IdFile.exists()) {
File oldV4IdFile = new File(finalApkFile.getAbsolutePath() + ".idsig");
if (oldV4IdFile.exists()) {
oldV4IdFile.delete();
}
FileOperation.copyFileUsingStream(v4IdFile, oldV4IdFile);
}
}
}
private void insureFileNameV1() {
mUnSignedApk = new File(mOutDir.getAbsolutePath(), mApkName + "_unsigned.apk");
mSignedWith7ZipApk = new File(mOutDir.getAbsolutePath(), mApkName + "_signed_7zip.apk");
mSignedApk = new File(mOutDir.getAbsolutePath(), mApkName + "_signed.apk");
mAlignedApk = new File(mOutDir.getAbsolutePath(), mApkName + "_signed_aligned.apk");
mAlignedWith7ZipApk = new File(mOutDir.getAbsolutePath(), mApkName + "_signed_7zip_aligned.apk");
m7zipOutPutDir = new File(mOutDir.getAbsolutePath(), TypedValue.OUT_7ZIP_FILE_PATH);
}
private void insureFileNameV2() {
mUnSignedApk = new File(mOutDir.getAbsolutePath(), mApkName + "_unsigned.apk");
m7ZipApk = new File(mOutDir.getAbsolutePath(), mApkName + "_7zip_unsigned.apk");
if (config.mUse7zip) {
mAlignedApk = new File(mOutDir.getAbsolutePath(), mApkName + "_7zip_aligned_unsigned.apk");
mSignedApk = new File(mOutDir.getAbsolutePath(), mApkName + "_7zip_aligned_signed.apk");
} else {
mAlignedApk = new File(mOutDir.getAbsolutePath(), mApkName + "_aligned_unsigned.apk");
mSignedApk = new File(mOutDir.getAbsolutePath(), mApkName + "_aligned_signed.apk");
}
m7zipOutPutDir = new File(mOutDir.getAbsolutePath(), TypedValue.OUT_7ZIP_FILE_PATH);
}
private boolean use7zApk(HashMap compressData, File originalAPK, File outputAPK)
throws IOException, InterruptedException {
if (!config.mUse7zip) {
return false;
}
if (!config.mUseSignAPK) {
throw new IOException("if you want to use 7z, you must enable useSign in the config file first");
}
if (!originalAPK.exists()) {
throw new IOException(String.format("can not found the signed apk file to 7z, if you want to use 7z, "
+ "you must fill the sign data in the config file path=%s",
originalAPK.getAbsolutePath()
));
}
System.out.printf("use 7zip to repackage: %s, will cost much more time\n", outputAPK.getName());
FileOperation.unZipAPk(originalAPK.getAbsolutePath(), m7zipOutPutDir.getAbsolutePath());
//首先一次性生成一个全部都是压缩的安装包
generalRaw7zip(outputAPK);
ArrayList storedFiles = new ArrayList<>();
//对于不压缩的要update回去
for (String name : compressData.keySet()) {
File file = new File(m7zipOutPutDir.getAbsolutePath(), name);
if (!file.exists()) {
continue;
}
int method = compressData.get(name);
if (method == TypedValue.ZIP_STORED) {
storedFiles.add(name);
}
}
addStoredFileIn7Zip(storedFiles, outputAPK);
if (!outputAPK.exists()) {
throw new IOException(String.format(
"[use7zApk]7z repackage signed apk fail,you must install 7z command line version first, linux: p7zip, window: 7za, path=%s",
mSignedWith7ZipApk.getAbsolutePath()
));
}
return true;
}
private String getSignatureAlgorithm(String hash) throws Exception {
String signatureAlgorithm;
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
FileInputStream fileIn = new FileInputStream(config.mSignatureFile);
keyStore.load(fileIn, config.mStorePass.toCharArray());
Key key = keyStore.getKey(config.mStoreAlias, config.mKeyPass.toCharArray());
if (key == null) {
throw new RuntimeException("Can't get private key, please check if storepass storealias and keypass are correct");
}
String keyAlgorithm = key.getAlgorithm();
hash = formatHashAlgorithName(hash);
if (keyAlgorithm.equalsIgnoreCase("DSA")) {
keyAlgorithm = "DSA";
} else if (keyAlgorithm.equalsIgnoreCase("RSA")) {
keyAlgorithm = "RSA";
} else if (keyAlgorithm.equalsIgnoreCase("EC")) {
keyAlgorithm = "ECDSA";
} else {
throw new RuntimeException("private key is not a DSA or RSA key");
}
signatureAlgorithm = String.format("%swith%s", hash, keyAlgorithm);
return signatureAlgorithm;
}
private String formatHashAlgorithName(String hash) {
return hash.replace("-", "");
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private void signApkV1(File unSignedApk, File signedApk) throws IOException, InterruptedException {
if (config.mUseSignAPK) {
System.out.println("signing apk");
if (signedApk.exists()) {
signedApk.delete();
}
signWithV1sign(unSignedApk, signedApk);
if (!signedApk.exists()) {
throw new IOException("Can't Generate signed APK. Plz check your v1sign info is correct.");
}
}
}
private void signApkV2V3(File unSignedApk, File signedApk, int minSDKVersion, InputParam.SignatureType signatureType) throws Exception {
if (config.mUseSignAPK) {
System.out.println("signing apk v2+");
signWithV2V3Sign(unSignedApk, signedApk, minSDKVersion, signatureType);
if (!signedApk.exists()) {
throw new IOException("Can't Generate signed APK v2. Plz check your v2sign info is correct.");
}
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private void signWithV2V3Sign(File unSignedApk, File signedApk, int minSDKVersion, InputParam.SignatureType signatureType) throws Exception {
String[] params;
if (!config.onlyV3V4Sign) {
params = new String[]{
"sign",
"--ks",
config.mSignatureFile.getAbsolutePath(),
"--ks-pass",
"pass:" + config.mStorePass,
"--min-sdk-version",
String.valueOf(minSDKVersion),
"--ks-key-alias",
config.mStoreAlias,
"--key-pass",
"pass:" + config.mKeyPass,
"--v3-signing-enabled",
String.valueOf(signatureType == SchemaV3),
"--out",
signedApk.getAbsolutePath(),
unSignedApk.getAbsolutePath()
};
ApkSignerTool.main(params);
} else {
String signTool = config.mZipalignPath.replace("zipalign", "apksigner.bat");
params = new String[]{
signTool,
"sign",
"--v1-signing-enabled",
"false",
"--v2-signing-enabled",
"false",
"--v3-signing-enabled",
"true",
"--v4-signing-enabled",
"true",
"--ks",
config.mSignatureFile.getAbsolutePath(),
"--ks-pass",
"pass:" + config.mStorePass,
"--min-sdk-version",
String.valueOf(minSDKVersion),
"--ks-key-alias",
config.mStoreAlias,
"--key-pass",
"pass:" + config.mKeyPass,
unSignedApk.getAbsolutePath(),
};
// System.out.println(String.join(" ", params).replace(config.mStorePass,"***").replace(config.mKeyPass,"***"));
Utils.runCmd(params);
if (signedApk.exists()) {
signedApk.delete();
}
unSignedApk.renameTo(signedApk);
}
}
private void signWithV1sign(File unSignedApk, File signedApk) throws IOException, InterruptedException {
String signatureAlgorithm = "MD5withRSA";
try {
signatureAlgorithm = getSignatureAlgorithm(config.digestAlg);
} catch (Exception e) {
e.fillInStackTrace();
}
String[] argv = {
"jarsigner",
"-sigalg",
signatureAlgorithm,
"-digestalg",
config.digestAlg,
"-keystore",
config.mSignatureFile.getAbsolutePath(),
"-storepass",
config.mStorePass,
"-keypass",
config.mKeyPass,
"-signedjar",
signedApk.getAbsolutePath(),
unSignedApk.getAbsolutePath(),
config.mStoreAlias
};
Utils.runExec(argv);
}
private void alignApks() throws IOException, InterruptedException {
//如果不签名就肯定不需要对齐了
if (!config.mUseSignAPK) {
return;
}
if (!mSignedApk.exists() && !mSignedWith7ZipApk.exists()) {
throw new IOException("Can not found any signed apk file");
}
if (mSignedApk.exists()) {
alignApk(mSignedApk, mAlignedApk);
}
if (mSignedWith7ZipApk.exists()) {
alignApk(mSignedWith7ZipApk, mAlignedWith7ZipApk);
}
}
private void alignApk(File before, File after) throws IOException, InterruptedException {
System.out.println("zipAligning apk");
if (!before.exists()) {
throw new IOException(String.format("can not found the raw apk file to zipalign, path=%s",
before.getAbsolutePath()
));
}
String cmd = Utils.isPresent(config.mZipalignPath) ? config.mZipalignPath : TypedValue.COMMAND_ZIPALIGIN;
Utils.runCmd(cmd, "4", before.getAbsolutePath(), after.getAbsolutePath());
if (!after.exists()) {
throw new IOException(String.format("can not found the aligned apk file, the ZipAlign path is correct? path=%s",
mAlignedApk.getAbsolutePath()
));
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private void generalUnsignApk(HashMap compressData) throws IOException {
System.out.printf("General unsigned apk: %s\n", mUnSignedApk.getName());
File tempOutDir = new File(mOutDir.getAbsolutePath(), TypedValue.UNZIP_FILE_PATH);
if (!tempOutDir.exists()) {
System.err.printf("Missing apk unzip files, path=%s\n", tempOutDir.getAbsolutePath());
System.exit(-1);
}
File[] unzipFiles = tempOutDir.listFiles();
assert unzipFiles != null;
List collectFiles = new ArrayList<>();
for (File f : unzipFiles) {
String name = f.getName();
if (name.equals("META-INF") && config.mDelMetaInf) {
f.delete();
continue;
}
if (name.equals("res") || name.equals("resources.arsc")) {
continue;
} else if (name.equals(config.mMetaName)) {
addNonSignatureFiles(collectFiles, f);
continue;
}
collectFiles.add(f);
}
File destResDir = new File(mOutDir.getAbsolutePath(), "res");
//添加修改后的res文件
if (!config.mKeepRoot && FileOperation.getlist(destResDir) == 0) {
destResDir = new File(mOutDir.getAbsolutePath(), TypedValue.RES_FILE_PATH);
}
/*
* NOTE:文件数量应该是一样的,如果不一样肯定有问题
*/
File rawResDir = new File(tempOutDir.getAbsolutePath() + File.separator + "res");
System.out.printf("DestResDir %d rawResDir %d\n",
FileOperation.getlist(destResDir),
FileOperation.getlist(rawResDir)
);
if (FileOperation.getlist(destResDir) != (FileOperation.getlist(rawResDir) - ARSCDecoder.mMergeDuplicatedResCount)) {
throw new IOException(String.format(
"the file count of %s, and the file count of %s is not equal, there must be some problem\n",
rawResDir.getAbsolutePath(),
destResDir.getAbsolutePath()
));
}
if (!destResDir.exists()) {
System.err.printf("Missing res files, path=%s\n", destResDir.getAbsolutePath());
System.exit(-1);
}
//这个需要检查混淆前混淆后,两个res的文件数量是否相等
collectFiles.add(destResDir);
File rawARSCFile = new File(mOutDir.getAbsolutePath() + File.separator + "resources.arsc");
if (!rawARSCFile.exists()) {
System.err.printf("Missing resources.arsc files, path=%s\n", rawARSCFile.getAbsolutePath());
System.exit(-1);
}
collectFiles.add(rawARSCFile);
FileOperation.zipFiles(collectFiles, tempOutDir, mUnSignedApk, compressData);
if (!mUnSignedApk.exists()) {
throw new IOException(String.format("can not found the unsign apk file path=%s", mUnSignedApk.getAbsolutePath()));
}
}
private void addNonSignatureFiles(List collectFiles, File metaFolder) {
File[] metaFiles = metaFolder.listFiles();
if (metaFiles != null) {
for (File metaFile : metaFiles) {
String metaFileName = metaFile.getName();
// Ignore signature files
if (!metaFileName.endsWith(".MF") && !metaFileName.endsWith(".RSA") && !metaFileName.endsWith(".SF")) {
System.out.printf("add meta file %s%n", metaFile.getAbsolutePath());
collectFiles.add(metaFile);
}
}
}
}
private void addStoredFileIn7Zip(ArrayList storedFiles, File outSevenZipAPK)
throws IOException, InterruptedException {
System.out.printf("[addStoredFileIn7Zip]rewrite the stored file into the 7zip, file count: %d\n",
storedFiles.size()
);
if (storedFiles.isEmpty()) return;
String storedParentName = mOutDir.getAbsolutePath() + File.separator + "storefiles" + File.separator;
String outputName = m7zipOutPutDir.getAbsolutePath() + File.separator;
for (String name : storedFiles) {
FileOperation.copyFileUsingStream(new File(outputName + name), new File(storedParentName + name));
}
storedParentName = storedParentName + File.separator + "*";
String cmd = Utils.isPresent(config.m7zipPath) ? config.m7zipPath : TypedValue.COMMAND_7ZIP;
Utils.runCmd(cmd, "a", "-tzip", outSevenZipAPK.getAbsolutePath(), storedParentName, "-mx0");
}
private void generalRaw7zip(File outSevenZipApk) throws IOException, InterruptedException {
String outPath = m7zipOutPutDir.getAbsoluteFile().getAbsolutePath();
String path = outPath + File.separator + "*";
String cmd = Utils.isPresent(config.m7zipPath) ? config.m7zipPath : TypedValue.COMMAND_7ZIP;
Utils.runCmd(cmd, "a", "-tzip", outSevenZipApk.getAbsolutePath(), path, "-mx9");
}
}