Repository: shwenzhang/AndResGuard Branch: master Commit: e4df245d82f2 Files: 146 Total size: 399.6 KB Directory structure: gitextract_al65wnr7/ ├── .gitignore ├── .travis.yml ├── AndResGuard-cli/ │ ├── build.gradle │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── tencent/ │ └── mm/ │ └── resourceproguard/ │ └── cli/ │ └── CliMain.java ├── AndResGuard-core/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ └── java/ │ ├── apksigner/ │ │ ├── ApkSignerTool.java │ │ ├── HexEncoding.java │ │ ├── OptionsParser.java │ │ ├── PasswordRetriever.java │ │ ├── help.txt │ │ ├── help_sign.txt │ │ └── help_verify.txt │ └── com/ │ ├── mindprod/ │ │ └── ledatastream/ │ │ ├── LEDataInputStream.java │ │ ├── LEDataOutputStream.java │ │ └── LittleEndianDataOutputStream.java │ └── tencent/ │ └── mm/ │ ├── androlib/ │ │ ├── AndrolibException.java │ │ ├── ApkDecoder.java │ │ ├── ResourceApkBuilder.java │ │ ├── ResourceRepackage.java │ │ └── res/ │ │ ├── data/ │ │ │ ├── ResID.java │ │ │ ├── ResPackage.java │ │ │ └── ResType.java │ │ ├── decoder/ │ │ │ ├── ARSCDecoder.java │ │ │ ├── RawARSCDecoder.java │ │ │ └── StringBlock.java │ │ └── util/ │ │ ├── ExtFile.java │ │ └── StringUtil.java │ ├── directory/ │ │ ├── AbstractDirectory.java │ │ ├── Directory.java │ │ ├── DirectoryException.java │ │ ├── FileDirectory.java │ │ ├── PathAlreadyExists.java │ │ ├── PathNotExist.java │ │ └── ZipRODirectory.java │ ├── resourceproguard/ │ │ ├── Configuration.java │ │ ├── InputParam.java │ │ └── Main.java │ └── util/ │ ├── DataInputDelegate.java │ ├── DataOutputDelegate.java │ ├── ExtDataInput.java │ ├── ExtDataOutput.java │ ├── FileOperation.java │ ├── Md5Util.java │ ├── TypedValue.java │ └── Utils.java ├── AndResGuard-example/ │ ├── .gitignore │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ ├── resource_mapping.txt │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── andresguard/ │ │ │ └── tencent/ │ │ │ └── com/ │ │ │ └── andresguard_example/ │ │ │ └── ApplicationTest.java │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── andresguard/ │ │ │ │ └── tencent/ │ │ │ │ └── com/ │ │ │ │ └── andresguard_example/ │ │ │ │ └── MainActivity.java │ │ │ └── res/ │ │ │ └── .keep │ │ └── test/ │ │ └── java/ │ │ └── andresguard/ │ │ └── tencent/ │ │ └── com/ │ │ └── andresguard_example/ │ │ └── ExampleUnitTest.java │ ├── app1/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ ├── resource_mapping.txt │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── andresguard/ │ │ │ └── tencent/ │ │ │ └── com/ │ │ │ └── andresguard_example/ │ │ │ └── ApplicationTest.java │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── andresguard/ │ │ │ │ └── tencent/ │ │ │ │ └── com/ │ │ │ │ └── andresguard_example/ │ │ │ │ └── MainActivity.java │ │ │ └── res/ │ │ │ └── .keep │ │ └── test/ │ │ └── java/ │ │ └── andresguard/ │ │ └── tencent/ │ │ └── com/ │ │ └── andresguard_example/ │ │ └── ExampleUnitTest.java │ ├── app2/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ ├── resource_mapping.txt │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── andresguard/ │ │ │ └── tencent/ │ │ │ └── com/ │ │ │ └── andresguard_example/ │ │ │ └── ApplicationTest.java │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── java/ │ │ │ │ └── andresguard/ │ │ │ │ └── tencent/ │ │ │ │ └── com/ │ │ │ │ └── andresguard_example/ │ │ │ │ └── MainActivity.java │ │ │ └── res/ │ │ │ └── .keep │ │ └── test/ │ │ └── java/ │ │ └── andresguard/ │ │ └── tencent/ │ │ └── com/ │ │ └── andresguard_example/ │ │ └── ExampleUnitTest.java │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── keystore test/ │ │ ├── debug.keystore │ │ ├── release.keystore │ │ └── testKey.jks │ ├── libres/ │ │ ├── .gitignore │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ ├── androidTest/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── tinkerpatch/ │ │ │ └── libres/ │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── side_nav_bar.xml │ │ │ ├── drawable-v21/ │ │ │ │ ├── ic_menu_camera.xml │ │ │ │ ├── ic_menu_gallery.xml │ │ │ │ ├── ic_menu_manage.xml │ │ │ │ ├── ic_menu_send.xml │ │ │ │ ├── ic_menu_share.xml │ │ │ │ └── ic_menu_slideshow.xml │ │ │ ├── layout/ │ │ │ │ ├── activity_main.xml │ │ │ │ ├── app_bar_main.xml │ │ │ │ ├── content_main.xml │ │ │ │ └── nav_header_main.xml │ │ │ ├── menu/ │ │ │ │ ├── activity_main_drawer.xml │ │ │ │ └── main.xml │ │ │ ├── values/ │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── drawables.xml │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── values-v21/ │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ ├── values-w820dp/ │ │ │ │ ├── dimens.xml │ │ │ │ └── strings.xml │ │ │ └── values-xxhdpi/ │ │ │ └── strings.xml │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── tinkerpatch/ │ │ └── libres/ │ │ └── ExampleUnitTest.java │ └── settings.gradle ├── AndResGuard-gradle-plugin/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── groovy/ │ │ └── com/ │ │ └── tencent/ │ │ └── gradle/ │ │ ├── AndResGuardExtension.groovy │ │ ├── AndResGuardPlugin.groovy │ │ ├── AndResGuardTask.groovy │ │ ├── BuildInfo.groovy │ │ └── ExecutorExtension.groovy │ └── resources/ │ └── META-INF/ │ └── gradle-plugins/ │ └── AndResGuard.properties ├── LICENSE ├── README.md ├── README.zh-cn.md ├── SECURITY.md ├── SevenZip/ │ ├── build.gradle │ └── gradle.properties ├── appveyol.yml ├── build.gradle ├── doc/ │ ├── how_to_work.md │ ├── how_to_work.zh-cn.md │ └── white_list.md ├── gradle/ │ ├── gradle-mvn-push.gradle │ ├── java-artifacts.gradle │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── package.json ├── settings.gradle └── tool_output/ ├── AndResGuard-cli-1.2.15.jar ├── build_apk.bat ├── build_apk.sh ├── config.xml └── release.keystore ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.ap_ # Files for the Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ # Gradle files .gradle/ build/ /*/build/ *.iml /*/*.iml .idea /*/.idea/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log .DS_Store work/outapk/* local_repo tool_output/outapk node_modules/ classes AndResGuard-example/true final.apk ================================================ FILE: .travis.yml ================================================ language: java dist: trusty script: "./gradlew check" jdk: - oraclejdk8 notifications: webhooks: urls: - https://webhooks.gitter.im/e/cbb5207fafff92a021b7 on_success: change # options: [always|never|change] default: always on_failure: always # options: [always|never|change] default: always on_start: never # options: [always|never|change] default: always ================================================ FILE: AndResGuard-cli/build.gradle ================================================ apply plugin: 'java' [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' version rootProject.ext.VERSION_NAME group rootProject.ext.GROUP dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) //compile group: 'com.tencent.mm', name: 'AndResGuard-core', version: version compile project(':AndResGuard-core') } sourceSets { main { java { srcDir 'src' } } } jar { manifest { attributes 'Main-Class': 'com.tencent.mm.resourceproguard.cli.CliMain' attributes 'Manifest-Version': version attributes "Jar-Version": "${ANDRESGUARD_VESSION}" attributes "Build-Time": releaseTime() } from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } } // copy the jar to work directory task buildJar(type: Copy, dependsOn: [build, jar]) { from('build/libs') { include '*' + version + '*.jar' } into('../tool_output') } def releaseTime() { return new Date().format("yyyy-MM-dd HH:mm ZZZ", TimeZone.getDefault()) } defaultTasks 'buildJar' ================================================ FILE: AndResGuard-cli/src/main/java/com/tencent/mm/resourceproguard/cli/CliMain.java ================================================ package com.tencent.mm.resourceproguard.cli; import com.tencent.mm.androlib.ResourceRepackage; import com.tencent.mm.resourceproguard.Configuration; import com.tencent.mm.resourceproguard.InputParam; import com.tencent.mm.resourceproguard.Main; import com.tencent.mm.util.TypedValue; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.SAXException; /** * Created by simsun on 1/9/16. */ public class CliMain extends Main { private static final String ARG_HELP = "--help"; private static final String ARG_OUT = "-out"; private static final String ARG_FINAL_APK_PATH = "-finalApkPath"; private static final String ARG_CONFIG = "-config"; private static final String ARG_7ZIP = "-7zip"; private static final String ARG_ZIPALIGN = "-zipalign"; private static final String ARG_SIGNATURE = "-signature"; private static final String ARG_KEEPMAPPING = "-mapping"; private static final String ARG_REPACKAGE = "-repackage"; private static final String ARG_SIGNATURE_TYPE = "-signatureType"; private static final String VALUE_SIGNATURE_TYPE_V1 = "v1"; private static final String VALUE_SIGNATURE_TYPE_V2 = "v2"; public static void main(String[] args) { mBeginTime = System.currentTimeMillis(); CliMain m = new CliMain(); setRunningLocation(m); m.run(args); } private static void setRunningLocation(CliMain m) { mRunningLocation = m.getClass().getProtectionDomain().getCodeSource().getLocation().getPath(); try { mRunningLocation = URLDecoder.decode(mRunningLocation, "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } if (mRunningLocation.endsWith(".jar")) { mRunningLocation = mRunningLocation.substring(0, mRunningLocation.lastIndexOf(File.separator) + 1); } File f = new File(mRunningLocation); mRunningLocation = f.getAbsolutePath(); } private static void printUsage(PrintStream out) { // TODO: Look up launcher script name! String command = "resousceproguard.jar"; //$NON-NLS-1$ out.println(); out.println(); out.println("Usage: java -jar " + command + " input.apk"); out.println("if you want to special the output path or config file path, you can input:"); out.println("Such as: java -jar " + command + " " + "input.apk " + ARG_CONFIG + " yourconfig.xml " + ARG_OUT + " output_directory"); out.println("if you want to special the sign or mapping data, you can input:"); out.println("Such as: java -jar " + command + " " + "input.apk " + ARG_CONFIG + " yourconfig.xml " + ARG_OUT + " output_directory " + ARG_SIGNATURE + " signature_file_path storepass keypass storealias " + ARG_KEEPMAPPING + " mapping_file_path"); out.println("if you want to special the signature type, you can input:"); out.printf("Such as: java -jar %s input.apk %s %s/%s\n", command, ARG_SIGNATURE_TYPE, VALUE_SIGNATURE_TYPE_V1, VALUE_SIGNATURE_TYPE_V2 ); out.println("if you want to special 7za or zipalign path, you can input:"); out.println("Such as: java -jar " + command + " " + "input.apk " + ARG_7ZIP + " /home/shwenzhang/tools/7za " + ARG_ZIPALIGN + " /home/shwenzhang/sdk/tools/zipalign"); out.println("if you just want to repackage an apk compress with 7z:"); out.println("Such as: java -jar " + command + " " + ARG_REPACKAGE + " input.apk"); out.println("if you want to special the output path, 7za or zipalign path, you can input:"); out.println("Such as: java -jar " + command + " " + ARG_REPACKAGE + " input.apk" + ARG_OUT + " output_directory " + ARG_7ZIP + " /home/shwenzhang/tools/7za " + ARG_ZIPALIGN + "/home/shwenzhang/sdk/tools/zipalign"); out.println("if you want to special the final apk path, you can input:"); out.printf("Such as: java -jar %s input.apk %s final_apk_path\n", command, ARG_FINAL_APK_PATH); out.println(); out.println("Flags:\n"); printUsage(out, new String[] { ARG_HELP, "This message.", "-h", "short for -help", ARG_OUT, "set the output directory yourself, if not, the default directory is the running location with name of the input file", ARG_CONFIG, "set the config file yourself, if not, the default path is the running location with name config.xml", ARG_SIGNATURE, "set sign property, following by parameters: signature_file_path storepass keypass storealias", " ", "if you set these, the sign data in the config file will be overlayed", ARG_KEEPMAPPING, "set keep mapping property, following by parameters: mapping_file_path", " ", "if you set these, the mapping data in the config file will be overlayed", ARG_7ZIP, "set the 7zip path, such as /home/shwenzhang/tools/7za, window will be end of 7za.exe", ARG_ZIPALIGN, "set the zipalign, such as /home/shwenzhang/sdk/tools/zipalign, window will be end of zipalign.exe", ARG_REPACKAGE, "usually, when we build the channeles apk, it may destroy the 7zip.", " ", "so you may need to use 7zip to repackage the apk", }); out.println(); out.println("if you donot know how to write the config file, look at the comment in the default config.xml"); out.println("if you want to use 7z, you must install the 7z command line version in window;"); out.println("sudo apt-get install p7zip-full in linux"); } private static void printUsage(PrintStream out, String[] args) { int argWidth = 0; for (int i = 0; i < args.length; i += 2) { String arg = args[i]; argWidth = Math.max(argWidth, arg.length()); } argWidth += 2; StringBuilder sb = new StringBuilder(); for (int i = 0; i < argWidth; i++) { sb.append(' '); } String indent = sb.toString(); String formatString = "%1$-" + argWidth + "s%2$s"; //$NON-NLS-1$ for (int i = 0; i < args.length; i += 2) { String arg = args[i]; String description = args[i + 1]; if (arg.length() == 0) { out.println(description); } else { out.print(wrap(String.format(formatString, arg, description), 300, indent)); } } } private static String wrap(String explanation, int lineWidth, String hangingIndent) { int explanationLength = explanation.length(); StringBuilder sb = new StringBuilder(explanationLength * 2); int index = 0; while (index < explanationLength) { int lineEnd = explanation.indexOf('\n', index); int next; if (lineEnd != -1 && (lineEnd - index) < lineWidth) { next = lineEnd + 1; } else { // Line is longer than available width; grab as much as we can lineEnd = Math.min(index + lineWidth, explanationLength); if (lineEnd - index < lineWidth) { next = explanationLength; } else { // then back up to the last space int lastSpace = explanation.lastIndexOf(' ', lineEnd); if (lastSpace > index) { lineEnd = lastSpace; next = lastSpace + 1; } else { // No space anywhere on the line: it contains something wider than // can fit (like a long URL) so just hard break it next = lineEnd + 1; } } } if (sb.length() > 0) { sb.append(hangingIndent); } else { lineWidth -= hangingIndent.length(); } sb.append(explanation.substring(index, lineEnd)); sb.append('\n'); index = next; } return sb.toString(); } private void run(String[] args) { synchronized (CliMain.class) { if (args.length < 1) { goToError(); } final ReadArgs readArgs = new ReadArgs(args).invoke(); final File configFile = readArgs.getConfigFile(); final File signatureFile = readArgs.getSignatureFile(); final File mappingFile = readArgs.getMappingFile(); final String keypass = readArgs.getKeypass(); final String storealias = readArgs.getStorealias(); final String storepass = readArgs.getStorepass(); final String signedFile = readArgs.getSignedFile(); final File outputFile = readArgs.getOutputFile(); final File finalApkFile = readArgs.getFinalApkFile(); final String apkFileName = readArgs.getApkFileName(); final InputParam.SignatureType signatureType = readArgs.getSignatureType(); loadConfigFromXml(configFile, signatureFile, mappingFile, keypass, storealias, storepass); //对于repackage模式,不管之前的东东,直接return if (signedFile != null) { ResourceRepackage repackage = new ResourceRepackage(config.mZipalignPath, config.m7zipPath, new File(signedFile) ); try { if (outputFile != null) { repackage.setOutDir(outputFile); } repackage.repackageApk(); } catch (IOException | InterruptedException e) { e.printStackTrace(); } return; } System.out.printf("[AndResGuard] begin: %s, %s, %s\n", outputFile, finalApkFile, apkFileName); resourceProguard(outputFile, finalApkFile, apkFileName, signatureType); System.out.printf("[AndResGuard] done, total time cost: %fs\n", diffTimeFromBegin()); System.out.printf("[AndResGuard] done, you can go to file to find the output %s\n", mOutDir.getAbsolutePath()); clean(); } } private void loadConfigFromXml( File configFile, File signatureFile, File mappingFile, String keypass, String storealias, String storepass) { if (configFile == null) { configFile = new File(mRunningLocation + File.separator + TypedValue.CONFIG_FILE); if (!configFile.exists()) { System.err.printf("the config file %s does not exit", configFile.getAbsolutePath()); printUsage(System.err); System.exit(ERRNO_USAGE); } } try { //不需要检查命令行的设置 if (!mSetSignThroughCmd) { signatureFile = null; } if (!mSetMappingThroughCmd) { mappingFile = null; } config = new Configuration(configFile, m7zipPath, mZipalignPath, mappingFile, signatureFile, keypass, storealias, storepass ); } catch (IOException | ParserConfigurationException | SAXException e) { e.printStackTrace(); goToError(); } } public double diffTimeFromBegin() { long end = System.currentTimeMillis(); return (end - mBeginTime) / 1000.0; } protected void goToError() { printUsage(System.err); System.exit(ERRNO_USAGE); } private class ReadArgs { private String[] args; private File configFile; private File outputFile; private File finalApkFile; private String apkFileName; private File signatureFile; private File mappingFile; private String keypass; private String storealias; private String storepass; private InputParam.SignatureType signatureType = InputParam.SignatureType.SchemaV1; private String signedFile; public ReadArgs(String[] args) { this.args = args; } public File getConfigFile() { return configFile; } public File getOutputFile() { return outputFile; } public File getFinalApkFile() { return finalApkFile; } public String getApkFileName() { return apkFileName; } public File getSignatureFile() { return signatureFile; } public File getMappingFile() { return mappingFile; } public String getKeypass() { return keypass; } public String getStorealias() { return storealias; } public String getStorepass() { return storepass; } public InputParam.SignatureType getSignatureType() { return signatureType; } public String getSignedFile() { return signedFile; } public ReadArgs invoke() { for (int index = 0; index < args.length; index++) { String arg = args[index]; if (arg.equals(ARG_HELP) || arg.equals("-h")) { goToError(); } else if (arg.equals(ARG_CONFIG)) { if (index == args.length - 1 || !args[index + 1].endsWith(TypedValue.XML_FILE)) { System.err.println("Missing XML configuration file argument"); goToError(); } configFile = new File(args[++index]); if (!configFile.exists()) { System.err.println(configFile.getAbsolutePath() + " does not exist"); goToError(); } System.out.printf("special configFile file path: %s\n", configFile.getAbsolutePath()); } else if (arg.equals(ARG_OUT)) { if (index == args.length - 1) { System.err.println("Missing output file argument"); goToError(); } outputFile = new File(args[++index]); File parent = outputFile.getParentFile(); if (parent != null && (!parent.exists())) { parent.mkdirs(); } System.out.printf("special output directory path: %s\n", outputFile.getAbsolutePath()); } else if (arg.equals(ARG_FINAL_APK_PATH)) { if (index == args.length - 1) { System.err.println("Missing output file argument"); goToError(); } finalApkFile = new File(args[++index]); File parent = finalApkFile.getParentFile(); if (parent != null && (!parent.exists())) { parent.mkdirs(); } System.out.printf("special final apk file path: %s\n", finalApkFile.getAbsolutePath()); } else if (arg.equals(ARG_SIGNATURE)) { //需要检查是否有四个参数 if (index == args.length - 1) { System.err.println("Missing signature data argument, should be " + ARG_SIGNATURE + " signature_file_path storepass keypass storealias"); goToError(); } //在后面设置的时候会检查文件是否存在 signatureFile = new File(args[++index]); if (index == args.length - 1) { System.err.println("Missing signature data argument, should be " + ARG_SIGNATURE + " signature_file_path storepass keypass storealias"); goToError(); } storepass = args[++index]; if (index == args.length - 1) { System.err.println("Missing signature data argument, should be " + ARG_SIGNATURE + " signature_file_path storepass keypass storealias"); goToError(); } keypass = args[++index]; if (index == args.length - 1) { System.err.println("Missing signature data argument, should be " + ARG_SIGNATURE + " signature_file_path storepass keypass storealias"); goToError(); } storealias = args[++index]; mSetSignThroughCmd = true; } else if (arg.equals(ARG_SIGNATURE_TYPE)) { if (index == args.length - 1) { System.err.println("Missing signature type argument"); goToError(); } if (VALUE_SIGNATURE_TYPE_V2.equalsIgnoreCase(args[++index])) { signatureType = InputParam.SignatureType.SchemaV2; } else { signatureType = InputParam.SignatureType.SchemaV1; } } else if (arg.equals(ARG_KEEPMAPPING)) { if (index == args.length - 1) { System.err.println("Missing mapping file argument"); goToError(); } //在后面设置的时候会检查文件是否存在 mappingFile = new File(args[++index]); mSetMappingThroughCmd = true; } else if (arg.equals(ARG_7ZIP)) { if (index == args.length - 1) { System.err.println("Missing 7zip path argument"); goToError(); } m7zipPath = args[++index]; } else if (arg.equals(ARG_ZIPALIGN)) { if (index == args.length - 1) { System.err.println("Missing zipalign path argument"); goToError(); } mZipalignPath = args[++index]; } else if (arg.equals(ARG_REPACKAGE)) { //这个模式的话就直接干活了,不会再理其他命令! if (index == args.length - 1) { System.err.println("Missing the signed apk file argument"); goToError(); } signedFile = args[++index]; } else { apkFileName = arg; } } return this; } } } ================================================ FILE: AndResGuard-core/build.gradle ================================================ apply plugin: 'java' version rootProject.ext.VERSION_NAME group rootProject.ext.GROUP [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.tools.build:gradle:4.1.2' compile 'commons-io:commons-io:2.6' } sourceSets { main { java { srcDir 'src' } } } [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' apply from: rootProject.file('gradle/java-artifacts.gradle') apply from: rootProject.file('gradle/gradle-mvn-push.gradle') ================================================ FILE: AndResGuard-core/gradle.properties ================================================ POM_ARTIFACT_ID=AndResGuard-core POM_NAME=AndResGuard Core POM_PACKAGING=jar ================================================ FILE: AndResGuard-core/src/main/java/apksigner/ApkSignerTool.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package apksigner; import com.android.apksig.ApkSigner; import com.android.apksig.ApkVerifier; import com.android.apksig.apk.MinSdkVersionException; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyFactory; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Provider; import java.security.PublicKey; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.interfaces.DSAKey; import java.security.interfaces.DSAParams; import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.List; import javax.crypto.EncryptedPrivateKeyInfo; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; /** * Command-line tool for signing APKs and for checking whether an APK's signature are expected to * verify on Android devices. */ public class ApkSignerTool { private static final String VERSION = "0.5"; private static final String HELP_PAGE_GENERAL = "help.txt"; private static final String HELP_PAGE_SIGN = "help_sign.txt"; private static final String HELP_PAGE_VERIFY = "help_verify.txt"; public static void main(String[] params) throws Exception { if ((params.length == 0) || ("--help".equals(params[0])) || ("-h".equals(params[0]))) { printUsage(HELP_PAGE_GENERAL); return; } else if ("--version".equals(params[0])) { System.out.println(VERSION); return; } String cmd = params[0]; try { if ("sign".equals(cmd)) { sign(Arrays.copyOfRange(params, 1, params.length)); return; } else if ("verify".equals(cmd)) { verify(Arrays.copyOfRange(params, 1, params.length)); return; } else if ("help".equals(cmd)) { printUsage(HELP_PAGE_GENERAL); return; } else if ("version".equals(cmd)) { System.out.println(VERSION); return; } else { throw new ParameterException("Unsupported command: " + cmd + ". See --help for supported commands"); } } catch (ParameterException | OptionsParser.OptionsException e) { System.err.println(e.getMessage()); System.exit(1); return; } } private static void sign(String[] params) throws Exception { if (params.length == 0) { printUsage(HELP_PAGE_SIGN); return; } File outputApk = null; File inputApk = null; boolean verbose = false; boolean v1SigningEnabled = true; boolean v2SigningEnabled = true; boolean v3SigningEnabled = false; int minSdkVersion = 1; boolean minSdkVersionSpecified = false; int maxSdkVersion = Integer.MAX_VALUE; List signers = new ArrayList<>(1); SignerParams signerParams = new SignerParams(); OptionsParser optionsParser = new OptionsParser(params); String optionName; String optionOriginalForm = null; while ((optionName = optionsParser.nextOption()) != null) { optionOriginalForm = optionsParser.getOptionOriginalForm(); if (("help".equals(optionName)) || ("h".equals(optionName))) { printUsage(HELP_PAGE_SIGN); return; } else if ("out".equals(optionName)) { outputApk = new File(optionsParser.getRequiredValue("Output file name")); } else if ("in".equals(optionName)) { inputApk = new File(optionsParser.getRequiredValue("Input file name")); } else if ("min-sdk-version".equals(optionName)) { minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level"); minSdkVersionSpecified = true; } else if ("max-sdk-version".equals(optionName)) { maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level"); } else if ("v1-signing-enabled".equals(optionName)) { v1SigningEnabled = optionsParser.getOptionalBooleanValue(true); } else if ("v2-signing-enabled".equals(optionName)) { v2SigningEnabled = optionsParser.getOptionalBooleanValue(true); } else if ("v3-signing-enabled".equals(optionName)) { v3SigningEnabled = optionsParser.getOptionalBooleanValue(false); } else if ("next-signer".equals(optionName)) { if (!signerParams.isEmpty()) { signers.add(signerParams); signerParams = new SignerParams(); } } else if ("ks".equals(optionName)) { signerParams.keystoreFile = optionsParser.getRequiredValue("KeyStore file"); } else if ("ks-key-alias".equals(optionName)) { signerParams.keystoreKeyAlias = optionsParser.getRequiredValue("KeyStore key alias"); } else if ("ks-pass".equals(optionName)) { signerParams.keystorePasswordSpec = optionsParser.getRequiredValue("KeyStore password"); } else if ("key-pass".equals(optionName)) { signerParams.keyPasswordSpec = optionsParser.getRequiredValue("Key password"); } else if ("v1-signer-name".equals(optionName)) { signerParams.v1SigFileBasename = optionsParser.getRequiredValue("JAR signature file basename"); } else if ("ks-type".equals(optionName)) { signerParams.keystoreType = optionsParser.getRequiredValue("KeyStore type"); } else if ("ks-provider-name".equals(optionName)) { signerParams.keystoreProviderName = optionsParser.getRequiredValue("JCA KeyStore Provider name"); } else if ("ks-provider-class".equals(optionName)) { signerParams.keystoreProviderClass = optionsParser.getRequiredValue("JCA KeyStore Provider class name"); } else if ("ks-provider-arg".equals(optionName)) { signerParams.keystoreProviderArg = optionsParser.getRequiredValue("JCA KeyStore Provider constructor argument"); } else if ("key".equals(optionName)) { signerParams.keyFile = optionsParser.getRequiredValue("Private key file"); } else if ("cert".equals(optionName)) { signerParams.certFile = optionsParser.getRequiredValue("Certificate file"); } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) { verbose = optionsParser.getOptionalBooleanValue(true); } else { throw new ParameterException("Unsupported option: " + optionOriginalForm + ". See --help for supported" + " options."); } } if (!signerParams.isEmpty()) { signers.add(signerParams); } signerParams = null; if (signers.isEmpty()) { throw new ParameterException("At least one signer must be specified"); } params = optionsParser.getRemainingParams(); if (inputApk != null) { // Input APK has been specified via preceding parameters. We don't expect any more // parameters. if (params.length > 0) { throw new ParameterException("Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]); } } else { // Input APK has not been specified via preceding parameters. The next parameter is // supposed to be the path to input APK. if (params.length < 1) { throw new ParameterException("Missing input APK"); } else if (params.length > 1) { throw new ParameterException("Unexpected parameter(s) after input APK (" + params[1] + ")"); } inputApk = new File(params[0]); } if ((minSdkVersionSpecified) && (minSdkVersion > maxSdkVersion)) { throw new ParameterException("Min API Level (" + minSdkVersion + ") > max API Level (" + maxSdkVersion + ")"); } List signerConfigs = new ArrayList<>(signers.size()); int signerNumber = 0; try (PasswordRetriever passwordRetriever = new PasswordRetriever()) { for (SignerParams signer : signers) { signerNumber++; signer.name = "signer #" + signerNumber; try { signer.loadPrivateKeyAndCerts(passwordRetriever); } catch (ParameterException e) { System.err.println("Failed to load signer \"" + signer.name + "\": " + e.getMessage()); System.exit(2); return; } catch (Exception e) { System.err.println("Failed to load signer \"" + signer.name + "\""); e.printStackTrace(); System.exit(2); return; } String v1SigBasename; if (signer.v1SigFileBasename != null) { v1SigBasename = signer.v1SigFileBasename; } else if (signer.keystoreKeyAlias != null) { v1SigBasename = signer.keystoreKeyAlias; } else if (signer.keyFile != null) { String keyFileName = new File(signer.keyFile).getName(); int delimiterIndex = keyFileName.indexOf('.'); if (delimiterIndex == -1) { v1SigBasename = keyFileName; } else { v1SigBasename = keyFileName.substring(0, delimiterIndex); } } else { throw new RuntimeException("Neither KeyStore key alias nor private key file available"); } ApkSigner.SignerConfig signerConfig = new ApkSigner.SignerConfig.Builder(v1SigBasename, signer.privateKey, signer.certs ).build(); signerConfigs.add(signerConfig); } } if (outputApk == null) { outputApk = inputApk; } File tmpOutputApk; if (inputApk.getCanonicalPath().equals(outputApk.getCanonicalPath())) { tmpOutputApk = File.createTempFile("apksigner", ".apk"); tmpOutputApk.deleteOnExit(); } else { tmpOutputApk = outputApk; } ApkSigner.Builder apkSignerBuilder = new ApkSigner.Builder(signerConfigs).setInputApk(inputApk) .setOutputApk(tmpOutputApk) .setOtherSignersSignaturesPreserved(false) .setV1SigningEnabled(v1SigningEnabled) .setV2SigningEnabled(v2SigningEnabled) .setV3SigningEnabled(v3SigningEnabled); if (minSdkVersionSpecified) { apkSignerBuilder.setMinSdkVersion(minSdkVersion); } ApkSigner apkSigner = apkSignerBuilder.build(); try { apkSigner.sign(); } catch (MinSdkVersionException e) { String msg = e.getMessage(); if (!msg.endsWith(".")) { msg += '.'; } throw new MinSdkVersionException("Failed to determine APK's minimum supported platform version" + ". Use --min-sdk-version to override", e); } if (!tmpOutputApk.getCanonicalPath().equals(outputApk.getCanonicalPath())) { Files.move(tmpOutputApk.toPath(), outputApk.toPath(), StandardCopyOption.REPLACE_EXISTING); } if (verbose) { System.out.println("Signed"); } } private static void verify(String[] params) throws Exception { if (params.length == 0) { printUsage(HELP_PAGE_VERIFY); return; } File inputApk = null; int minSdkVersion = 1; boolean minSdkVersionSpecified = false; int maxSdkVersion = Integer.MAX_VALUE; boolean maxSdkVersionSpecified = false; boolean printCerts = false; boolean verbose = false; boolean warningsTreatedAsErrors = false; OptionsParser optionsParser = new OptionsParser(params); String optionName; String optionOriginalForm = null; while ((optionName = optionsParser.nextOption()) != null) { optionOriginalForm = optionsParser.getOptionOriginalForm(); if ("min-sdk-version".equals(optionName)) { minSdkVersion = optionsParser.getRequiredIntValue("Mininimum API Level"); minSdkVersionSpecified = true; } else if ("max-sdk-version".equals(optionName)) { maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level"); maxSdkVersionSpecified = true; } else if ("print-certs".equals(optionName)) { printCerts = optionsParser.getOptionalBooleanValue(true); } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) { verbose = optionsParser.getOptionalBooleanValue(true); } else if ("Werr".equals(optionName)) { warningsTreatedAsErrors = optionsParser.getOptionalBooleanValue(true); } else if (("help".equals(optionName)) || ("h".equals(optionName))) { printUsage(HELP_PAGE_VERIFY); return; } else if ("in".equals(optionName)) { inputApk = new File(optionsParser.getRequiredValue("Input APK file")); } else { throw new ParameterException("Unsupported option: " + optionOriginalForm + ". See --help for supported" + " options."); } } params = optionsParser.getRemainingParams(); if (inputApk != null) { // Input APK has been specified in preceding parameters. We don't expect any more // parameters. if (params.length > 0) { throw new ParameterException("Unexpected parameter(s) after " + optionOriginalForm + ": " + params[0]); } } else { // Input APK has not been specified in preceding parameters. The next parameter is // supposed to be the input APK. if (params.length < 1) { throw new ParameterException("Missing APK"); } else if (params.length > 1) { throw new ParameterException("Unexpected parameter(s) after APK (" + params[1] + ")"); } inputApk = new File(params[0]); } if ((minSdkVersionSpecified) && (maxSdkVersionSpecified) && (minSdkVersion > maxSdkVersion)) { throw new ParameterException("Min API Level (" + minSdkVersion + ") > max API Level (" + maxSdkVersion + ")"); } ApkVerifier.Builder apkVerifierBuilder = new ApkVerifier.Builder(inputApk); if (minSdkVersionSpecified) { apkVerifierBuilder.setMinCheckedPlatformVersion(minSdkVersion); } if (maxSdkVersionSpecified) { apkVerifierBuilder.setMaxCheckedPlatformVersion(maxSdkVersion); } ApkVerifier apkVerifier = apkVerifierBuilder.build(); ApkVerifier.Result result; try { result = apkVerifier.verify(); } catch (MinSdkVersionException e) { String msg = e.getMessage(); if (!msg.endsWith(".")) { msg += '.'; } throw new MinSdkVersionException("Failed to determine APK's minimum supported platform version" + ". Use --min-sdk-version to override", e); } boolean verified = result.isVerified(); boolean warningsEncountered = false; if (verified) { List signerCerts = result.getSignerCertificates(); if (verbose) { System.out.println("Verifies"); System.out.println("Verified using v1 scheme (JAR signing): " + result.isVerifiedUsingV1Scheme()); System.out.println("Verified using v2 scheme (APK Signature Scheme v2): " + result.isVerifiedUsingV2Scheme()); System.out.println("Number of signers: " + signerCerts.size()); } if (printCerts) { int signerNumber = 0; MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); MessageDigest md5 = MessageDigest.getInstance("MD5"); for (X509Certificate signerCert : signerCerts) { signerNumber++; System.out.println("Signer #" + signerNumber + " certificate DN" + ": " + signerCert.getSubjectDN()); byte[] encodedCert = signerCert.getEncoded(); System.out.println("Signer #" + signerNumber + " certificate SHA-256 digest: " + HexEncoding.encode(sha256.digest(encodedCert))); System.out.println("Signer #" + signerNumber + " certificate SHA-1 digest: " + HexEncoding.encode(sha1.digest( encodedCert))); System.out.println("Signer #" + signerNumber + " certificate MD5 digest: " + HexEncoding.encode(md5.digest( encodedCert))); if (verbose) { PublicKey publicKey = signerCert.getPublicKey(); System.out.println("Signer #" + signerNumber + " key algorithm: " + publicKey.getAlgorithm()); int keySize = -1; if (publicKey instanceof RSAKey) { keySize = ((RSAKey) publicKey).getModulus().bitLength(); } else if (publicKey instanceof ECKey) { keySize = ((ECKey) publicKey).getParams().getOrder().bitLength(); } else if (publicKey instanceof DSAKey) { // DSA parameters may be inherited from the certificate. We // don't handle this case at the moment. DSAParams dsaParams = ((DSAKey) publicKey).getParams(); if (dsaParams != null) { keySize = dsaParams.getP().bitLength(); } } System.out.println("Signer #" + signerNumber + " key size (bits): " + ((keySize != -1) ? String.valueOf( keySize) : "n/a")); byte[] encodedKey = publicKey.getEncoded(); System.out.println("Signer #" + signerNumber + " public key SHA-256 digest: " + HexEncoding.encode(sha256.digest(encodedKey))); System.out.println("Signer #" + signerNumber + " public key SHA-1 digest: " + HexEncoding.encode(sha1.digest(encodedKey))); System.out.println("Signer #" + signerNumber + " public key MD5 digest: " + HexEncoding.encode(md5.digest( encodedKey))); } } } } else { System.err.println("DOES NOT VERIFY"); } for (ApkVerifier.IssueWithParams error : result.getErrors()) { System.err.println("ERROR: " + error); } @SuppressWarnings("resource") // false positive -- this resource is not opened here PrintStream warningsOut = (warningsTreatedAsErrors) ? System.err : System.out; for (ApkVerifier.IssueWithParams warning : result.getWarnings()) { warningsEncountered = true; warningsOut.println("WARNING: " + warning); } for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) { String signerName = signer.getName(); for (ApkVerifier.IssueWithParams error : signer.getErrors()) { System.err.println("ERROR: JAR signer " + signerName + ": " + error); } for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) { warningsEncountered = true; warningsOut.println("WARNING: JAR signer " + signerName + ": " + warning); } } for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) { String signerName = "signer #" + (signer.getIndex() + 1); for (ApkVerifier.IssueWithParams error : signer.getErrors()) { System.err.println("ERROR: APK Signature Scheme v2 " + signerName + ": " + error); } for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) { warningsEncountered = true; warningsOut.println("WARNING: APK Signature Scheme v2 " + signerName + ": " + warning); } } if (!verified) { System.exit(1); return; } if ((warningsTreatedAsErrors) && (warningsEncountered)) { System.exit(1); return; } } private static void printUsage(String page) { try (BufferedReader in = new BufferedReader(new InputStreamReader( ApkSignerTool.class.getResourceAsStream(page), StandardCharsets.UTF_8 ))) { String line; while ((line = in.readLine()) != null) { System.out.println(line); } } catch (IOException e) { throw new RuntimeException("Failed to read " + page + " resource"); } } private static byte[] readFully(File file) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); try (FileInputStream in = new FileInputStream(file)) { drain(in, result); } return result.toByteArray(); } private static void drain(InputStream in, OutputStream out) throws IOException { byte[] buf = new byte[65536]; int chunkSize; while ((chunkSize = in.read(buf)) != -1) { out.write(buf, 0, chunkSize); } } private static class SignerParams { String name; String keystoreFile; String keystoreKeyAlias; String keystorePasswordSpec; String keyPasswordSpec; String keystoreType; String keystoreProviderName; String keystoreProviderClass; String keystoreProviderArg; String keyFile; String certFile; String v1SigFileBasename; PrivateKey privateKey; List certs; private static void loadKeyStoreFromFile(KeyStore ks, String file, List passwords) throws Exception { Exception lastFailure = null; for (char[] password : passwords) { try { try (FileInputStream in = new FileInputStream(file)) { ks.load(in, password); } return; } catch (Exception e) { lastFailure = e; } } if (lastFailure == null) { throw new RuntimeException("No keystore passwords"); } else { throw lastFailure; } } private static Key getKeyStoreKey(KeyStore ks, String keyAlias, List passwords) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException { UnrecoverableKeyException lastFailure = null; for (char[] password : passwords) { try { return ks.getKey(keyAlias, password); } catch (UnrecoverableKeyException e) { lastFailure = e; } } if (lastFailure == null) { throw new RuntimeException("No key passwords"); } else { throw lastFailure; } } private static PKCS8EncodedKeySpec decryptPkcs8EncodedKey( EncryptedPrivateKeyInfo encryptedPrivateKeyInfo, List passwords) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); InvalidKeySpecException lastKeySpecException = null; InvalidKeyException lastKeyException = null; for (char[] password : passwords) { PBEKeySpec decryptionKeySpec = new PBEKeySpec(password); try { SecretKey decryptionKey = keyFactory.generateSecret(decryptionKeySpec); return encryptedPrivateKeyInfo.getKeySpec(decryptionKey); } catch (InvalidKeySpecException e) { lastKeySpecException = e; } catch (InvalidKeyException e) { lastKeyException = e; } } if ((lastKeyException == null) && (lastKeySpecException == null)) { throw new RuntimeException("No passwords"); } else if (lastKeyException != null) { throw lastKeyException; } else { throw lastKeySpecException; } } private static PrivateKey loadPkcs8EncodedPrivateKey(PKCS8EncodedKeySpec spec) throws InvalidKeySpecException, NoSuchAlgorithmException { try { return KeyFactory.getInstance("RSA").generatePrivate(spec); } catch (InvalidKeySpecException expected) { } try { return KeyFactory.getInstance("EC").generatePrivate(spec); } catch (InvalidKeySpecException expected) { } try { return KeyFactory.getInstance("DSA").generatePrivate(spec); } catch (InvalidKeySpecException expected) { } throw new InvalidKeySpecException("Not an RSA, EC, or DSA private key"); } private boolean isEmpty() { return (name == null) && (keystoreFile == null) && (keystoreKeyAlias == null) && (keystorePasswordSpec == null) && (keyPasswordSpec == null) && (keystoreType == null) && (keystoreProviderName == null) && (keystoreProviderClass == null) && (keystoreProviderArg == null) && (keyFile == null) && (certFile == null) && (v1SigFileBasename == null) && (privateKey == null) && (certs == null); } private void loadPrivateKeyAndCerts(PasswordRetriever passwordRetriever) throws Exception { if (keystoreFile != null) { if (keyFile != null) { throw new ParameterException("--ks and --key may not be specified at the same time"); } else if (certFile != null) { throw new ParameterException("--ks and --cert may not be specified at the same time"); } loadPrivateKeyAndCertsFromKeyStore(passwordRetriever); } else if (keyFile != null) { loadPrivateKeyAndCertsFromFiles(passwordRetriever); } else { throw new ParameterException("KeyStore (--ks) or private key file (--key) must be specified"); } } private void loadPrivateKeyAndCertsFromKeyStore(PasswordRetriever passwordRetriever) throws Exception { if (keystoreFile == null) { throw new ParameterException("KeyStore (--ks) must be specified"); } // 1. Obtain a KeyStore implementation String ksType = (keystoreType != null) ? keystoreType : KeyStore.getDefaultType(); KeyStore ks; if (keystoreProviderName != null) { // Use a named Provider (assumes the provider is already installed) ks = KeyStore.getInstance(ksType, keystoreProviderName); } else if (keystoreProviderClass != null) { // Use a new Provider instance (does not require the provider to be installed) Class ksProviderClass = Class.forName(keystoreProviderClass); if (!Provider.class.isAssignableFrom(ksProviderClass)) { throw new ParameterException("Keystore Provider class " + keystoreProviderClass + " not subclass of " + Provider.class.getName()); } Provider ksProvider; if (keystoreProviderArg != null) { // Single-arg Provider constructor ksProvider = (Provider) ksProviderClass.getConstructor(String.class).newInstance(keystoreProviderArg); } else { // No-arg Provider constructor ksProvider = (Provider) ksProviderClass.getConstructor().newInstance(); } ks = KeyStore.getInstance(ksType, ksProvider); } else { // Use the highest-priority Provider which offers the requested KeyStore type ks = KeyStore.getInstance(ksType); } // 2. Load the KeyStore List keystorePasswords = null; if ("NONE".equals(keystoreFile)) { ks.load(null); } else { String keystorePasswordSpec = (this.keystorePasswordSpec != null) ? this.keystorePasswordSpec : PasswordRetriever.SPEC_STDIN; keystorePasswords = passwordRetriever.getPasswords(keystorePasswordSpec, "Keystore password for " + name); loadKeyStoreFromFile(ks, keystoreFile, keystorePasswords); } // 3. Load the PrivateKey and cert chain from KeyStore String keyAlias = null; PrivateKey key = null; try { if (keystoreKeyAlias == null) { // Private key entry alias not specified. Find the key entry contained in this // KeyStore. If the KeyStore contains multiple key entries, return an error. Enumeration aliases = ks.aliases(); if (aliases != null) { while (aliases.hasMoreElements()) { String entryAlias = aliases.nextElement(); if (ks.isKeyEntry(entryAlias)) { keyAlias = entryAlias; if (keystoreKeyAlias != null) { throw new ParameterException(keystoreFile + " contains multiple key entries" + ". --ks-key-alias option must be used to specify" + " which entry to use."); } keystoreKeyAlias = keyAlias; } } } if (keystoreKeyAlias == null) { throw new ParameterException(keystoreFile + " does not contain key entries"); } } // Private key entry alias known. Load that entry's private key. keyAlias = keystoreKeyAlias; if (!ks.isKeyEntry(keyAlias)) { throw new ParameterException(keystoreFile + " entry \"" + keyAlias + "\" does not contain a key"); } Key entryKey; if (keyPasswordSpec != null) { // Key password spec is explicitly specified. Use this spec to obtain the // password and then load the key using that password. List keyPasswords = passwordRetriever.getPasswords(keyPasswordSpec, "Key \"" + keyAlias + "\" password for " + name ); entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords); } else { // Key password spec is not specified. This means we should assume that key // password is the same as the keystore password and that, if this assumption is // wrong, we should prompt for key password and retry loading the key using that // password. try { entryKey = getKeyStoreKey(ks, keyAlias, keystorePasswords); } catch (UnrecoverableKeyException expected) { List keyPasswords = passwordRetriever.getPasswords(PasswordRetriever.SPEC_STDIN, "Key \"" + keyAlias + "\" password for " + name ); entryKey = getKeyStoreKey(ks, keyAlias, keyPasswords); } } if (entryKey == null) { throw new ParameterException(keystoreFile + " entry \"" + keyAlias + "\" does not contain a key"); } else if (!(entryKey instanceof PrivateKey)) { throw new ParameterException(keystoreFile + " entry \"" + keyAlias + "\" does not contain a private" + " key. It contains a key of algorithm: " + entryKey.getAlgorithm()); } key = (PrivateKey) entryKey; } catch (UnrecoverableKeyException e) { throw new IOException("Failed to obtain key with alias \"" + keyAlias + "\" from " + keystoreFile + ". Wrong password?", e); } this.privateKey = key; Certificate[] certChain = ks.getCertificateChain(keyAlias); if ((certChain == null) || (certChain.length == 0)) { throw new ParameterException(keystoreFile + " entry \"" + keyAlias + "\" does not contain certificates"); } this.certs = new ArrayList<>(certChain.length); for (Certificate cert : certChain) { this.certs.add((X509Certificate) cert); } } private void loadPrivateKeyAndCertsFromFiles(PasswordRetriever passwordRetriver) throws Exception { if (keyFile == null) { throw new ParameterException("Private key file (--key) must be specified"); } if (certFile == null) { throw new ParameterException("Certificate file (--cert) must be specified"); } byte[] privateKeyBlob = readFully(new File(keyFile)); PKCS8EncodedKeySpec keySpec; // Potentially encrypted key blob try { EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(privateKeyBlob); // The blob is indeed an encrypted private key blob String passwordSpec = (keyPasswordSpec != null) ? keyPasswordSpec : PasswordRetriever.SPEC_STDIN; List keyPasswords = passwordRetriver.getPasswords(passwordSpec, "Private key password for " + name); keySpec = decryptPkcs8EncodedKey(encryptedPrivateKeyInfo, keyPasswords); } catch (IOException e) { // The blob is not an encrypted private key blob if (keyPasswordSpec == null) { // Given that no password was specified, assume the blob is an unencrypted // private key blob keySpec = new PKCS8EncodedKeySpec(privateKeyBlob); } else { throw new InvalidKeySpecException("Failed to parse encrypted private key blob " + keyFile, e); } } // Load the private key from its PKCS #8 encoded form. try { privateKey = loadPkcs8EncodedPrivateKey(keySpec); } catch (InvalidKeySpecException e) { throw new InvalidKeySpecException("Failed to load PKCS #8 encoded private key from " + keyFile, e); } // Load certificates Collection certs; try (FileInputStream in = new FileInputStream(certFile)) { certs = CertificateFactory.getInstance("X.509").generateCertificates(in); } List certList = new ArrayList<>(certs.size()); for (Certificate cert : certs) { certList.add((X509Certificate) cert); } this.certs = certList; } } /** * Indicates that there is an issue with command-line parameters provided to this tool. */ private static class ParameterException extends Exception { private static final long serialVersionUID = 1L; ParameterException(String message) { super(message); } } } ================================================ FILE: AndResGuard-core/src/main/java/apksigner/HexEncoding.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package apksigner; import java.nio.ByteBuffer; /** * Hexadecimal encoding where each byte is represented by two hexadecimal digits. */ class HexEncoding { private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); /** * Hidden constructor to prevent instantiation. */ private HexEncoding() { } /** * Encodes the provided data as a hexadecimal string. */ public static String encode(byte[] data, int offset, int length) { StringBuilder result = new StringBuilder(length * 2); for (int i = 0; i < length; i++) { byte b = data[offset + i]; result.append(HEX_DIGITS[(b >>> 4) & 0x0f]); result.append(HEX_DIGITS[b & 0x0f]); } return result.toString(); } /** * Encodes the provided data as a hexadecimal string. */ public static String encode(byte[] data) { return encode(data, 0, data.length); } /** * Encodes the remaining bytes of the provided {@link ByteBuffer} as a hexadecimal string. */ public static String encodeRemaining(ByteBuffer data) { return encode(data.array(), data.arrayOffset() + data.position(), data.remaining()); } } ================================================ FILE: AndResGuard-core/src/main/java/apksigner/OptionsParser.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package apksigner; import java.util.Arrays; /** * Parser of command-line options/switches/flags. * *

Supported option formats: *

    *
  • {@code --name value}
  • *
  • {@code --name=value}
  • *
  • {@code -name value}
  • *
  • {@code --name} (boolean options only)
  • *
* *

To use the parser, create an instance, providing it with the command-line parameters, then * iterate over options by invoking {@link #nextOption()} until it returns {@code null}. */ class OptionsParser { private final String[] mParams; private int mIndex; private String mLastOptionValue; private String mLastOptionOriginalForm; /** * Constructs a new {@code OptionsParser} initialized with the provided command-line. */ public OptionsParser(String[] params) { mParams = params.clone(); } /** * Returns the name (without leading dashes) of the next option (starting with the very first * option) or {@code null} if there are no options left. * *

The value of this option can be obtained via {@link #getRequiredValue(String)}, * {@link #getRequiredIntValue(String)}, and {@link #getOptionalBooleanValue(boolean)}. */ public String nextOption() { if (mIndex >= mParams.length) { // No more parameters left return null; } String param = mParams[mIndex]; if (!param.startsWith("-")) { // Not an option return null; } mIndex++; mLastOptionOriginalForm = param; mLastOptionValue = null; if (param.startsWith("--")) { // FORMAT: --name value OR --name=value if ("--".equals(param)) { // End of options marker return null; } int valueDelimiterIndex = param.indexOf('='); if (valueDelimiterIndex != -1) { mLastOptionValue = param.substring(valueDelimiterIndex + 1); mLastOptionOriginalForm = param.substring(0, valueDelimiterIndex); return param.substring("--".length(), valueDelimiterIndex); } else { return param.substring("--".length()); } } else { // FORMAT: -name value return param.substring("-".length()); } } /** * Returns the original form of the current option. The original form includes the leading dash * or dashes. This is intended to be used for referencing the option in error messages. */ public String getOptionOriginalForm() { return mLastOptionOriginalForm; } /** * Returns the value of the current option, throwing an exception if the value is missing. */ public String getRequiredValue(String valueDescription) throws OptionsException { if (mLastOptionValue != null) { String result = mLastOptionValue; mLastOptionValue = null; return result; } if (mIndex >= mParams.length) { // No more parameters left throw new OptionsException(valueDescription + " missing after " + mLastOptionOriginalForm); } String param = mParams[mIndex]; if ("--".equals(param)) { // End of options marker throw new OptionsException(valueDescription + " missing after " + mLastOptionOriginalForm); } mIndex++; return param; } /** * Returns the value of the current numeric option, throwing an exception if the value is * missing or is not numeric. */ public int getRequiredIntValue(String valueDescription) throws OptionsException { String value = getRequiredValue(valueDescription); try { return Integer.parseInt(value); } catch (NumberFormatException e) { throw new OptionsException(valueDescription + " (" + mLastOptionOriginalForm + ") must be a decimal number: " + value); } } /** * Gets the value of the current boolean option. Boolean options are not required to have * explicitly specified values. */ public boolean getOptionalBooleanValue(boolean defaultValue) throws OptionsException { if (mLastOptionValue != null) { // --option=value form String stringValue = mLastOptionValue; mLastOptionValue = null; if ("true".equals(stringValue)) { return true; } else if ("false".equals(stringValue)) { return false; } throw new OptionsException("Unsupported value for " + mLastOptionOriginalForm + ": " + stringValue + ". Only true or false supported."); } // --option (true|false) form OR just --option if (mIndex >= mParams.length) { return defaultValue; } String stringValue = mParams[mIndex]; if ("true".equals(stringValue)) { mIndex++; return true; } else if ("false".equals(stringValue)) { mIndex++; return false; } else { return defaultValue; } } /** * Returns the remaining command-line parameters. This is intended to be invoked once * {@link #nextOption()} returns {@code null}. */ public String[] getRemainingParams() { if (mIndex >= mParams.length) { return new String[0]; } String param = mParams[mIndex]; if ("--".equals(param)) { // Skip end of options marker return Arrays.copyOfRange(mParams, mIndex + 1, mParams.length); } else { return Arrays.copyOfRange(mParams, mIndex, mParams.length); } } /** * Indicates that an error was encountered while parsing command-line options. */ public static class OptionsException extends Exception { private static final long serialVersionUID = 1L; public OptionsException(String message) { super(message); } } } ================================================ FILE: AndResGuard-core/src/main/java/apksigner/PasswordRetriever.java ================================================ /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package apksigner; import java.io.ByteArrayOutputStream; import java.io.Console; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Retriever of passwords based on password specs supported by {@code apksigner} tool. * *

apksigner supports retrieving multiple passwords from the same source (e.g., file, standard * input) which adds the need to keep some sources open across password retrievals. This class * addresses the need. * *

To use this retriever, construct a new instance, use {@link #getPasswords(String, String)} to * retrieve passwords, and then invoke {@link #close()} on the instance when done, enabling the * instance to release any held resources. */ class PasswordRetriever implements AutoCloseable { public static final String SPEC_STDIN = "stdin"; private static final Charset CONSOLE_CHARSET = getConsoleEncoding(); private final Map mFileInputStreams = new HashMap<>(); private boolean mClosed; /** * Returns the provided password and all password variants derived from the password. The * resulting list is guaranteed to contain at least one element. */ private static List getPasswords(char[] pwd) { List passwords = new ArrayList<>(3); addPasswords(passwords, pwd); return passwords; } /** * Returns the provided password and all password variants derived from the password. The * resulting list is guaranteed to contain at least one element. * * @param encodedPwd password encoded using the provided character encoding. * @param encodings character encodings in which the password is encoded in {@code encodedPwd}. */ private static List getPasswords(byte[] encodedPwd, Charset... encodings) { List passwords = new ArrayList<>(4); for (Charset encoding : encodings) { // Decode password and add it and its variants to the list try { char[] pwd = decodePassword(encodedPwd, encoding); addPasswords(passwords, pwd); } catch (IOException ignored) { } } // Add the original encoded form addPassword(passwords, castBytesToChars(encodedPwd)); return passwords; } /** * Adds the provided password and its variants to the provided list of passwords. * *

NOTE: This method adds only the passwords/variants which are not yet in the list. */ private static void addPasswords(List passwords, char[] pwd) { // Verbatim password addPassword(passwords, pwd); // Password encoded using the JVM default character encoding and upcast into char[] try { char[] encodedPwd = castBytesToChars(encodePassword(pwd, Charset.defaultCharset())); addPassword(passwords, encodedPwd); } catch (IOException ignored) { } // Password encoded using console character encoding and upcast into char[] if (!CONSOLE_CHARSET.equals(Charset.defaultCharset())) { try { char[] encodedPwd = castBytesToChars(encodePassword(pwd, CONSOLE_CHARSET)); addPassword(passwords, encodedPwd); } catch (IOException ignored) { } } } /** * Adds the provided password to the provided list. Does nothing if the password is already in * the list. */ private static void addPassword(List passwords, char[] password) { for (char[] existingPassword : passwords) { if (Arrays.equals(password, existingPassword)) { return; } } passwords.add(password); } private static byte[] encodePassword(char[] pwd, Charset cs) throws IOException { ByteBuffer pwdBytes = cs.newEncoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter( CodingErrorAction.REPLACE).encode(CharBuffer.wrap(pwd)); byte[] encoded = new byte[pwdBytes.remaining()]; pwdBytes.get(encoded); return encoded; } private static char[] decodePassword(byte[] pwdBytes, Charset encoding) throws IOException { CharBuffer pwdChars = encoding.newDecoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter( CodingErrorAction.REPLACE).decode(ByteBuffer.wrap(pwdBytes)); char[] result = new char[pwdChars.remaining()]; pwdChars.get(result); return result; } /** * Upcasts each {@code byte} in the provided array of bytes to a {@code char} and returns the * resulting array of characters. */ private static char[] castBytesToChars(byte[] bytes) { if (bytes == null) { return null; } char[] chars = new char[bytes.length]; for (int i = 0; i < bytes.length; i++) { chars[i] = (char) (bytes[i] & 0xff); } return chars; } /** * Returns the character encoding used by the console. */ private static Charset getConsoleEncoding() { // IMPLEMENTATION NOTE: There is no public API for obtaining the console's character // encoding. We thus cheat by using implementation details of the most popular JVMs. String consoleCharsetName; try { Method encodingMethod = Console.class.getDeclaredMethod("encoding"); encodingMethod.setAccessible(true); consoleCharsetName = (String) encodingMethod.invoke(null); if (consoleCharsetName == null) { return Charset.defaultCharset(); } } catch (ReflectiveOperationException e) { Charset defaultCharset = Charset.defaultCharset(); System.err.println("warning: Failed to obtain console character encoding name. Assuming " + defaultCharset); return defaultCharset; } try { return Charset.forName(consoleCharsetName); } catch (IllegalArgumentException e) { // On Windows 10, cp65001 is the UTF-8 code page. For some reason, popular JVMs don't // have a mapping for cp65001... if ("cp65001".equals(consoleCharsetName)) { return StandardCharsets.UTF_8; } Charset defaultCharset = Charset.defaultCharset(); System.err.println("warning: Console uses unknown character encoding: " + consoleCharsetName + ". Using " + defaultCharset + " instead"); return defaultCharset; } } private static byte[] readEncodedPassword(InputStream in) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); int b; while ((b = in.read()) != -1) { if (b == '\n') { break; } else if (b == '\r') { int next = in.read(); if ((next == -1) || (next == '\n')) { break; } if (!(in instanceof PushbackInputStream)) { in = new PushbackInputStream(in); } ((PushbackInputStream) in).unread(next); } result.write(b); } return result.toByteArray(); } /** * Returns the passwords described by the provided spec. The reason there may be more than one * password is compatibility with {@code keytool} and {@code jarsigner} which in certain cases * use the form of passwords encoded using the console's character encoding. * *

Supported specs: *

    *
  • stdin -- read password as a line from console, if available, or standard * input if console is not available
  • *
  • pass:password -- password specified inside the spec, starting after * {@code pass:}
  • *
  • file:path -- read password as a line from the specified file
  • *
  • env:name -- password is in the specified environment variable
  • *
* *

When the same file (including standard input) is used for providing multiple passwords, * the passwords are read from the file one line at a time. */ public List getPasswords(String spec, String description) throws IOException { // IMPLEMENTATION NOTE: Java KeyStore and PBEKeySpec APIs take passwords as arrays of // Unicode characters (char[]). Unfortunately, it appears that Sun/Oracle keytool and // jarsigner in some cases use passwords which are the encoded form obtained using the // console's character encoding. For example, if the encoding is UTF-8, keytool and // jarsigner will use the password which is obtained by upcasting each byte of the UTF-8 // encoded form to char. This occurs only when the password is read from stdin/console, and // does not occur when the password is read from a command-line parameter. // There are other tools which use the Java KeyStore API correctly. // Thus, for each password spec, there may be up to three passwords: // * Unicode characters, // * characters (upcast bytes) obtained from encoding the password using the console's // character encoding, // * characters (upcast bytes) obtained from encoding the password using the JVM's default // character encoding. // // For a sample password "\u0061\u0062\u00a1\u00e4\u044e\u0031": // On Windows 10 with English US as the UI language, IBM437 is used as console encoding and // windows-1252 is used as the JVM default encoding: // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000 // -alias test // generates a keystore and key which decrypt only with // "\u0061\u0062\u00ad\u0084\u003f\u0031" // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000 // -alias test -storepass // generates a keystore and key which decrypt only with // "\u0061\u0062\u00a1\u00e4\u003f\u0031" // On modern OSX/Linux UTF-8 is used as the console and JVM default encoding: // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000 // -alias test // generates a keystore and key which decrypt only with // "\u0061\u0062\u00c2\u00a1\u00c3\u00a4\u00d1\u008e\u0031" // * keytool -genkey -v -keystore native.jks -keyalg RSA -keysize 2048 -validity 10000 // -alias test // generates a keystore and key which decrypt only with // "\u0061\u0062\u00a1\u00e4\u044e\u0031" assertNotClosed(); if (spec.startsWith("pass:")) { char[] pwd = spec.substring("pass:".length()).toCharArray(); return getPasswords(pwd); } else if (SPEC_STDIN.equals(spec)) { Console console = System.console(); if (console != null) { // Reading from console char[] pwd = console.readPassword(description + ": "); if (pwd == null) { throw new IOException("Failed to read " + description + ": console closed"); } return getPasswords(pwd); } else { // Console not available -- reading from redirected input System.out.println(description + ": "); byte[] encodedPwd = readEncodedPassword(System.in); if (encodedPwd.length == 0) { throw new IOException("Failed to read " + description + ": standard input closed"); } // By default, textual input obtained via standard input is supposed to be decoded // using the in JVM default character encoding but we also try the console's // encoding just in case. return getPasswords(encodedPwd, Charset.defaultCharset(), CONSOLE_CHARSET); } } else if (spec.startsWith("file:")) { String name = spec.substring("file:".length()); File file = new File(name).getCanonicalFile(); InputStream in = mFileInputStreams.get(file); if (in == null) { in = new FileInputStream(file); mFileInputStreams.put(file, in); } byte[] encodedPwd = readEncodedPassword(in); if (encodedPwd.length == 0) { throw new IOException("Failed to read " + description + " : end of file reached in " + file); } // By default, textual input from files is supposed to be treated as encoded using JVM's // default character encoding. return getPasswords(encodedPwd, Charset.defaultCharset()); } else if (spec.startsWith("env:")) { String name = spec.substring("env:".length()); String value = System.getenv(name); if (value == null) { throw new IOException("Failed to read " + description + ": environment variable " + value + " not specified"); } return getPasswords(value.toCharArray()); } else { throw new IOException("Unsupported password spec for " + description + ": " + spec); } } private void assertNotClosed() { if (mClosed) { throw new IllegalStateException("Closed"); } } @Override public void close() { for (InputStream in : mFileInputStreams.values()) { try { in.close(); } catch (IOException ignored) { } } mFileInputStreams.clear(); mClosed = true; } } ================================================ FILE: AndResGuard-core/src/main/java/apksigner/help.txt ================================================ USAGE: apksigner [options] apksigner --version apksigner --help EXAMPLE: apksigner sign --ks release.jks app.apk apksigner verify --verbose app.apk apksigner is a tool for signing Android APK files and for checking whether signatures of APK files will verify on Android devices. COMMANDS sign Sign the provided APK verify Check whether the provided APK is expected to verify on Android version Show this tool's version number and exit help Show this usage page and exit ================================================ FILE: AndResGuard-core/src/main/java/apksigner/help_sign.txt ================================================ USAGE: apksigner sign [options] apk This signs the provided APK, stripping out any pre-existing signatures. Signing is performed using one or more signers, each represented by an asymmetric key pair and a corresponding certificate. Typically, an APK is signed by just one signer. For each signer, you need to provide the signer's private key and certificate. GENERAL OPTIONS --in Input APK file to sign. This is an alternative to specifying the APK as the very last parameter, after all options. Unless --out is specified, this file will be overwritten with the resulting signed APK. --out File into which to output the signed APK. By default, the APK is signed in-place, overwriting the input file. -v, --verbose Verbose output mode --v1-signing-enabled Whether to enable signing using JAR signing scheme (aka v1 signing scheme) used in Android since day one. By default, signing using this scheme is enabled based on min and max SDK version (see --min-sdk-version and --max-sdk-version). --v2-signing-enabled Whether to enable signing using APK Signature Scheme v2 (aka v2 signing scheme) introduced in Android Nougat, API Level 24. By default, signing using this scheme is enabled based on min and max SDK version (see --min-sdk-version and --max-sdk-version). --min-sdk-version Lowest API Level on which this APK's signatures will be verified. By default, the value from AndroidManifest.xml is used. The higher the value, the stronger security parameters are used when signing. --max-sdk-version Highest API Level on which this APK's signatures will be verified. By default, the highest possible value is used. -h, --help Show help about this command and exit PER-SIGNER OPTIONS These options specify the configuration of a particular signer. To delimit options of different signers, use --next-signer. --next-signer Delimits options of two different signers. There is no need to use this option when only one signer is used. --v1-signer-name Basename for files comprising the JAR signature scheme (aka v1 scheme) signature of this signer. By default, KeyStore key alias or basename of key file is used. PER-SIGNER SIGNING KEY & CERTIFICATE OPTIONS There are two ways to provide the signer's private key and certificate: (1) Java KeyStore (see --ks), or (2) private key file in PKCS #8 format and certificate file in X.509 format (see --key and --cert). --ks Load private key and certificate chain from the Java KeyStore initialized from the specified file. NONE means no file is needed by KeyStore, which is the case for some PKCS #11 KeyStores. --ks-key-alias Alias under which the private key and certificate are stored in the KeyStore. This must be specified if the KeyStore contains multiple keys. --ks-pass KeyStore password (see --ks). The following formats are supported: pass: password provided inline env: password provided in the named environment variable file: password provided in the named file, as a single line stdin password provided on standard input, as a single line A password is required to open a KeyStore. By default, the tool will prompt for password via console or standard input. When the same file (including standard input) is used for providing multiple passwords, the passwords are read from the file one line at a time. Passwords are read in the order in which signers are specified and, within each signer, KeyStore password is read before the key password is read. --key-pass Password with which the private key is protected. The following formats are supported: pass: password provided inline env: password provided in the named environment variable file: password provided in the named file, as a single line stdin password provided on standard input, as a single line If --key-pass is not specified for a KeyStore key, this tool will attempt to load the key using the KeyStore password and, if that fails, will prompt for key password and attempt to load the key using that password. If --key-pass is not specified for a private key file key, this tool will prompt for key password only if a password is required. When the same file (including standard input) is used for providing multiple passwords, the passwords are read from the file one line at a time. Passwords are read in the order in which signers are specified and, within each signer, KeyStore password is read before the key password is read. --ks-type Type/algorithm of KeyStore to use. By default, the default type is used. --ks-provider-name Name of the JCA Provider from which to request the KeyStore implementation. By default, the highest priority provider is used. See --ks-provider-class for the alternative way to specify a provider. --ks-provider-class Fully-qualified class name of the JCA Provider from which to request the KeyStore implementation. By default, the provider is chosen based on --ks-provider-name. --ks-provider-arg Value to pass into the constructor of the JCA Provider class specified by --ks-provider-class. The value is passed into the constructor as java.lang.String. By default, the no-arg provider's constructor is used. --key Load private key from the specified file. If the key is password-protected, the password will be prompted via standard input unless specified otherwise using --key-pass. The file must be in PKCS #8 DER format. --cert Load certificate chain from the specified file. The file must be in X.509 PEM or DER format. EXAMPLES 1. Sign an APK, in-place, using the one and only key in keystore release.jks: $ apksigner sign --ks release.jks app.apk 1. Sign an APK, without overwriting, using the one and only key in keystore release.jks: $ apksigner sign --ks release.jks --in app.apk --out app-signed.apk 3. Sign an APK using a private key and certificate stored as individual files: $ apksigner sign --key release.pk8 --cert release.x509.pem app.apk 4. Sign an APK using two keys: $ apksigner sign --ks release.jks --next-signer --ks magic.jks app.apk ================================================ FILE: AndResGuard-core/src/main/java/apksigner/help_verify.txt ================================================ USAGE: apksigner verify [options] apk This checks whether the provided APK will verify on Android. By default, this checks whether the APK will verify on all Android platform versions supported by the APK (as declared using minSdkVersion in AndroidManifest.xml). Use --min-sdk-version and/or --max-sdk-version to verify the APK against a custom range of API Levels. OPTIONS --print-certs Show information about the APK's signing certificates -v, --verbose Verbose output mode --min-sdk-version Lowest API Level on which this APK's signatures will be verified. By default, the value from AndroidManifest.xml is used. --max-sdk-version Highest API Level on which this APK's signatures will be verified. By default, the highest possible value is used. -Werr Treat warnings as errors --in APK file to verify. This is an alternative to specifying the APK as the very last parameter, after all options. -h, --help Show help about this command and exit EXAMPLES 1. Check whether the APK's signatures are expected to verify on all Android platforms declared as supported by this APK: $ apksigner verify app.apk 2. Check whether the APK's signatures are expected to verify on Android platforms with API Level 15 and higher: $ apksigner verify --min-sdk-version 15 app.apk ================================================ FILE: AndResGuard-core/src/main/java/com/mindprod/ledatastream/LEDataInputStream.java ================================================ /* * @(#)LEDataInputStream.java * * Summary: Little-Endian version of DataInputStream. * * Copyright: (c) 1998-2010 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.1+ * * Created with: IntelliJ IDEA IDE. * * Version History: * 1.8 2007-05-24 */ package com.mindprod.ledatastream; import java.io.DataInput; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; /** * Little-Endian version of DataInputStream. *

* Very similar to DataInputStream except it reads little-endian instead of * big-endian binary data. We can't extend DataInputStream directly since it has * only final methods, though DataInputStream itself is not final. This forces * us implement LEDataInputStream with a DataInputStream object, and use wrapper * methods. * * @author Roedy Green, Canadian Mind Products * @version 1.8 2007-05-24 * @since 1998 */ public final class LEDataInputStream implements DataInput { // ------------------------------ CONSTANTS ------------------------------ /** * undisplayed copyright notice. */ private static final String EMBEDDED_COPYRIGHT = "copyright (c) 1999-2010 Roedy Green, Canadian Mind Products, http://mindprod.com"; // ------------------------------ FIELDS ------------------------------ /** * to get at the big-Endian methods of a basic DataInputStream */ protected final DataInputStream dis; /** * to get at the a basic readBytes method. */ protected final InputStream is; /** * work array for buffering input. */ protected final byte[] work; // -------------------------- PUBLIC STATIC METHODS // -------------------------- /** * constructor. * * @param in binary inputstream of little-endian data. */ public LEDataInputStream(InputStream in) { this.is = in; this.dis = new DataInputStream(in); work = new byte[8]; } // -------------------------- PUBLIC INSTANCE METHODS // -------------------------- /** * Note. This is a STATIC method! * * @param in stream to read UTF chars from (endian irrelevant) * @return string from stream * @throws IOException if read fails. */ public static String readUTF(DataInput in) throws IOException { return DataInputStream.readUTF(in); } /** * close. * * @throws IOException if close fails. */ public final void close() throws IOException { dis.close(); } /** * Read bytes. Watch out, read may return fewer bytes than requested. * * @param ba where the bytes go. * @param off offset in buffer, not offset in file. * @param len count of bytes to read. * @return how many bytes read. * @throws IOException if read fails. */ public final int read(byte ba[], int off, int len) throws IOException { // For efficiency, we avoid one layer of wrapper return is.read(ba, off, len); } /** * read only a one-byte boolean. * * @return true or false. * @throws IOException if read fails. * @see java.io.DataInput#readBoolean() */ @Override public final boolean readBoolean() throws IOException { return dis.readBoolean(); } /** * read byte. * * @return the byte read. * @throws IOException if read fails. * @see java.io.DataInput#readByte() */ @Override public final byte readByte() throws IOException { return dis.readByte(); } /** * Read on char. like DataInputStream.readChar except little endian. * * @return little endian 16-bit unicode char from the stream. * @throws IOException if read fails. */ @Override public final char readChar() throws IOException { dis.readFully(work, 0, 2); return (char) ((work[1] & 0xff) << 8 | (work[0] & 0xff)); } /** * Read a double. like DataInputStream.readDouble except little endian. * * @return little endian IEEE double from the datastream. * @throws IOException ioexception */ @Override public final double readDouble() throws IOException { return Double.longBitsToDouble(readLong()); } /** * Read one float. Like DataInputStream.readFloat except little endian. * * @return little endian IEEE float from the datastream. * @throws IOException if read fails. */ @Override public final float readFloat() throws IOException { return Float.intBitsToFloat(readInt()); } /** * Read bytes until the array is filled. * * @see java.io.DataInput#readFully(byte[]) */ @Override public final void readFully(byte ba[]) throws IOException { dis.readFully(ba, 0, ba.length); } /** * Read bytes until the count is satisfied. * * @throws IOException if read fails. * @see java.io.DataInput#readFully(byte[], int, int) */ @Override public final void readFully(byte ba[], int off, int len) throws IOException { dis.readFully(ba, off, len); } /** * Read an int, 32-bits. Like DataInputStream.readInt except little endian. * * @return little-endian binary int from the datastream * @throws IOException if read fails. */ @Override public final int readInt() throws IOException { dis.readFully(work, 0, 4); return (work[3]) << 24 | (work[2] & 0xff) << 16 | (work[1] & 0xff) << 8 | (work[0] & 0xff); } /** * Read a line. * * @return a rough approximation of the 8-bit stream as a 16-bit unicode * string * @throws IOException ioexception * @deprecated This method does not properly convert bytes to characters. * Use a Reader instead with a little-endian encoding. */ @Deprecated @Override public final String readLine() throws IOException { return dis.readLine(); } /** * read a long, 64-bits. Like DataInputStream.readLong except little endian. * * @return little-endian binary long from the datastream. * @throws IOException ioexception */ @Override public final long readLong() throws IOException { dis.readFully(work, 0, 8); return (long) (work[7]) << 56 | /* long cast needed or shift done modulo 32 */ (long) (work[6] & 0xff) << 48 | (long) (work[5] & 0xff) << 40 | (long) (work[4] & 0xff) << 32 | (long) (work[3] & 0xff) << 24 | (long) (work[2] & 0xff) << 16 | (long) (work[1] & 0xff) << 8 | work[0] & 0xff; } /** * Read short, 16-bits. Like DataInputStream.readShort except little endian. * * @return little endian binary short from stream. * @throws IOException if read fails. */ @Override public final short readShort() throws IOException { dis.readFully(work, 0, 2); return (short) ((work[1] & 0xff) << 8 | (work[0] & 0xff)); } /** * Read UTF counted string. * * @return String read. */ @Override public final String readUTF() throws IOException { return dis.readUTF(); } /** * Read an unsigned byte. Note: returns an int, even though says Byte * (non-Javadoc) * * @throws IOException if read fails. * @see java.io.DataInput#readUnsignedByte() */ @Override public final int readUnsignedByte() throws IOException { return dis.readUnsignedByte(); } /** * Read an unsigned short, 16 bits. Like DataInputStream.readUnsignedShort * except little endian. Note, returns int even though it reads a short. * * @return little-endian int from the stream. * @throws IOException if read fails. */ @Override public final int readUnsignedShort() throws IOException { dis.readFully(work, 0, 2); return ((work[1] & 0xff) << 8 | (work[0] & 0xff)); } /** * Skip over bytes in the stream. See the general contract of the * skipBytes method of DataInput. *

* Bytes for this operation are read from the contained input stream. * * @param n the number of bytes to be skipped. * @return the actual number of bytes skipped. * @throws IOException if an I/O error occurs. */ @Override public final int skipBytes(int n) throws IOException { return dis.skipBytes(n); } } ================================================ FILE: AndResGuard-core/src/main/java/com/mindprod/ledatastream/LEDataOutputStream.java ================================================ /** * Description: * LEDataOutputStream.java Create on 2014-5-14 * * @author shaowenzhang * @version 1.0 * Copyright (c) 2014 Tecent WXG AndroidTeam. All Rights Reserved. */ package com.mindprod.ledatastream; import java.io.OutputStream; public class LEDataOutputStream extends LittleEndianDataOutputStream { public LEDataOutputStream(OutputStream out) { super(out); // TODO Auto-generated constructor stub } } ================================================ FILE: AndResGuard-core/src/main/java/com/mindprod/ledatastream/LittleEndianDataOutputStream.java ================================================ /** * Description: * LittleEndianDataOutputStream.java Create on 2014-5-14 * * @author shaowenzhang * @version 1.0 * Copyright (c) 2014 Tecent WXG AndroidTeam. All Rights Reserved. */ package com.mindprod.ledatastream; /** * Copyright (C) 2007 The Guava Authors *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.DataOutput; import java.io.DataOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; /*** * An implementation of {@link DataOutput} that uses little-endian byte ordering * for writing {@code char}, {@code short}, {@code int}, {@code float}, {@code * double}, and {@code long} values. *

* Note: This class intentionally violates the specification of its * supertype {@code DataOutput}, which explicitly requires big-endian byte * order. * * @author Chris Nokleberg * @author Keith Bottner * @since 8.0 */ public class LittleEndianDataOutputStream extends FilterOutputStream implements DataOutput { /*** * Creates a {@code LittleEndianDataOutputStream} that wraps the given stream. * * @param out the stream to delegate to */ public LittleEndianDataOutputStream(OutputStream out) { super(new DataOutputStream(out)); } /*** * Returns a big-endian representation of {@code value} in an 8-element byte * array; equivalent to {@code ByteBuffer.allocate(8).putLong(value).array()}. * For example, the input value {@code 0x1213141516171819L} would yield the * byte array {@code {0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19}}. * @param value long * @return byte array */ public static byte[] toByteArray(long value) { // Note that this code needs to stay compatible with GWT, which has known // bugs when narrowing byte casts of long values occur. byte[] result = new byte[8]; for (int i = 7; i >= 0; i--) { result[i] = (byte) (value & 0xffL); value >>= 8; } return result; } @Override public void write(byte[] b, int off, int len) throws IOException { // Override slow FilterOutputStream impl out.write(b, off, len); } @Override public void writeBoolean(boolean v) throws IOException { ((DataOutputStream) out).writeBoolean(v); } @Override public void writeByte(int v) throws IOException { ((DataOutputStream) out).writeByte(v); } /*** * @deprecated The semantics of {@code writeBytes(String s)} are considered * dangerous. Please use {@link #writeUTF(String s)}, * {@link #writeChars(String s)} or another write method instead. */ @Deprecated @Override public void writeBytes(String s) throws IOException { ((DataOutputStream) out).writeBytes(s); } /*** * Writes a char as specified by {@link DataOutputStream#writeChar(int)}, * except using little-endian byte order. * * @throws IOException if an I/O error occurs */ @Override public void writeChar(int v) throws IOException { writeShort(v); } /*** * Writes a {@code String} as specified by * {@link DataOutputStream#writeChars(String)}, except each character is * written using little-endian byte order. * * @throws IOException if an I/O error occurs */ @Override public void writeChars(String s) throws IOException { for (int i = 0; i < s.length(); i++) { writeChar(s.charAt(i)); } } /*** * Writes a {@code double} as specified by * {@link DataOutputStream#writeDouble(double)}, except using little-endian * byte order. * * @throws IOException if an I/O error occurs */ @Override public void writeDouble(double v) throws IOException { writeLong(Double.doubleToLongBits(v)); } /*** * Writes a {@code float} as specified by * {@link DataOutputStream#writeFloat(float)}, except using little-endian byte * order. * * @throws IOException if an I/O error occurs */ @Override public void writeFloat(float v) throws IOException { writeInt(Float.floatToIntBits(v)); } /*** * Writes an {@code int} as specified by * {@link DataOutputStream#writeInt(int)}, except using little-endian byte * order. * * @throws IOException if an I/O error occurs */ @Override public void writeInt(int v) throws IOException { out.write(0xFF & v); out.write(0xFF & (v >> 8)); out.write(0xFF & (v >> 16)); out.write(0xFF & (v >> 24)); } /*** * Writes a {@code long} as specified by * {@link DataOutputStream#writeLong(long)}, except using little-endian byte * order. * * @throws IOException if an I/O error occurs */ @Override public void writeLong(long v) throws IOException { byte[] bytes = toByteArray(Long.reverseBytes(v)); write(bytes, 0, bytes.length); } /*** * Writes a {@code short} as specified by * {@link DataOutputStream#writeShort(int)}, except using little-endian byte * order. * * @throws IOException if an I/O error occurs */ @Override public void writeShort(int v) throws IOException { out.write(0xFF & v); out.write(0xFF & (v >> 8)); } @Override public void writeUTF(String str) throws IOException { ((DataOutputStream) out).writeUTF(str); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/AndrolibException.java ================================================ package com.tencent.mm.androlib; /** * @author shwenzhang */ public class AndrolibException extends Exception { public AndrolibException() { } public AndrolibException(String message) { super(message); } public AndrolibException(String message, Throwable cause) { super(message, cause); } public AndrolibException(Throwable cause) { super(cause); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/ApkDecoder.java ================================================ package com.tencent.mm.androlib; import com.tencent.mm.androlib.res.data.ResPackage; import com.tencent.mm.androlib.res.decoder.ARSCDecoder; import com.tencent.mm.androlib.res.decoder.RawARSCDecoder; import com.tencent.mm.androlib.res.util.ExtFile; import com.tencent.mm.directory.DirectoryException; import com.tencent.mm.resourceproguard.Configuration; 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.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map.Entry; import java.util.regex.Pattern; /** * @author shwenzhang */ public class ApkDecoder { final HashSet mRawResourceFiles = new HashSet<>(); private final Configuration config; private final ExtFile apkFile; private File mOutDir; private File mOutTempARSCFile; private File mOutARSCFile; private File mOutResFile; private File mRawResFile; private File mOutTempDir; private File mResMappingFile; private File mMergeDuplicatedResMappingFile; private HashMap mCompressData; public ApkDecoder(Configuration config, File apkFile) { this.config = config; this.apkFile = new ExtFile(apkFile); } private void copyOtherResFiles() throws IOException { if (mRawResourceFiles.isEmpty()) { return; } Path resPath = mRawResFile.toPath(); Path destPath = mOutResFile.toPath(); for (Path path : mRawResourceFiles) { Path relativePath = resPath.relativize(path); Path dest = destPath.resolve(relativePath); System.out.printf("copy res file not in resources.arsc file:%s\n", relativePath.toString()); FileOperation.copyFileUsingStream(path.toFile(), dest.toFile()); } } public void removeCopiedResFile(Path key) { mRawResourceFiles.remove(key); } public Configuration getConfig() { return config; } public boolean hasResources() throws AndrolibException { try { return apkFile.getDirectory().containsFile("resources.arsc"); } catch (DirectoryException ex) { throw new AndrolibException(ex); } } private void ensureFilePath() throws IOException { Utils.cleanDir(mOutDir); String unZipDest = new File(mOutDir, TypedValue.UNZIP_FILE_PATH).getAbsolutePath(); System.out.printf("unziping apk to %s\n", unZipDest); mCompressData = FileOperation.unZipAPk(apkFile.getAbsoluteFile().getAbsolutePath(), unZipDest); dealWithCompressConfig(); //将res混淆成r if (!config.mKeepRoot) { mOutResFile = new File(mOutDir.getAbsolutePath() + File.separator + TypedValue.RES_FILE_PATH); } else { mOutResFile = new File(mOutDir.getAbsolutePath() + File.separator + "res"); } //这个需要混淆各个文件夹 mRawResFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + TypedValue.UNZIP_FILE_PATH + File.separator + "res"); mOutTempDir = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + TypedValue.UNZIP_FILE_PATH); //这里纪录原始res目录的文件 Files.walkFileTree(mRawResFile.toPath(), new ResourceFilesVisitor()); if (!mRawResFile.exists() || !mRawResFile.isDirectory()) { throw new IOException("can not found res dir in the apk or it is not a dir"); } mOutTempARSCFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + "resources_temp.arsc"); mOutARSCFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + "resources.arsc"); String basename = apkFile.getName().substring(0, apkFile.getName().indexOf(".apk")); mResMappingFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + TypedValue.RES_MAPPING_FILE + basename + TypedValue.TXT_FILE); mMergeDuplicatedResMappingFile = new File(mOutDir.getAbsoluteFile().getAbsolutePath() + File.separator + TypedValue.MERGE_DUPLICATED_RES_MAPPING_FILE + basename + TypedValue.TXT_FILE); } /** * 根据config来修改压缩的值 */ private void dealWithCompressConfig() { if (config.mUseCompress) { HashSet patterns = config.mCompressPatterns; if (!patterns.isEmpty()) { for (Entry entry : mCompressData.entrySet()) { String name = entry.getKey(); for (Iterator it = patterns.iterator(); it.hasNext(); ) { Pattern p = it.next(); if (p.matcher(name).matches()) { mCompressData.put(name, TypedValue.ZIP_DEFLATED); } } } } } } public HashMap getCompressData() { return mCompressData; } public File getOutDir() { return mOutDir; } public void setOutDir(File outDir) throws AndrolibException { mOutDir = outDir; } public File getOutResFile() { return mOutResFile; } public File getRawResFile() { return mRawResFile; } public File getOutTempARSCFile() { return mOutTempARSCFile; } public File getOutARSCFile() { return mOutARSCFile; } public File getOutTempDir() { return mOutTempDir; } public File getResMappingFile() { return mResMappingFile; } public File getMergeDuplicatedResMappingFile() { return mMergeDuplicatedResMappingFile; } public void decode() throws AndrolibException, IOException, DirectoryException { if (hasResources()) { ensureFilePath(); // read the resources.arsc checking for STORED vs DEFLATE compression // this will determine whether we compress on rebuild or not. System.out.printf("decoding resources.arsc\n"); RawARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc")); ResPackage[] pkgs = ARSCDecoder.decode(apkFile.getDirectory().getFileInput("resources.arsc"), this); //把没有纪录在resources.arsc的资源文件也拷进dest目录 copyOtherResFiles(); ARSCDecoder.write(apkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs); } } class ResourceFilesVisitor extends SimpleFileVisitor { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { mRawResourceFiles.add(file); return FileVisitResult.CONTINUE; } } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/ResourceApkBuilder.java ================================================ 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.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) throws AndrolibException { 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.println(String.format("Backup Final APk(V1) to %s", 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(); } private void copyFinalApkV2() throws IOException { if (mSignedApk.exists() && finalApkFile != null) { System.out.println(String.format("Backup Final APk(V2) to %s", finalApkFile)); FileOperation.copyFileUsingStream(mSignedApk, finalApkFile); } } 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("-", ""); } private void signApkV1(File unSignedApk, File signedApk) throws IOException, InterruptedException { if (config.mUseSignAPK) { System.out.printf("signing apk: %s\n", signedApk.getName()); 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.printf("signing apk: %s\n", signedApk.getName()); signWithV2V3Sign(unSignedApk, signedApk, minSDKVersion, signatureType); if (!signedApk.exists()) { throw new IOException("Can't Generate signed APK v2. Plz check your v2sign info is correct."); } } } private void signWithV2V3Sign(File unSignedApk, File signedApk, int minSDKVersion, InputParam.SignatureType signatureType) throws Exception { String[] 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); } private void signWithV1sign(File unSignedApk, File signedApk) throws IOException, InterruptedException { String signatureAlgorithm = "MD5withRSA"; try { signatureAlgorithm = getSignatureAlgorithm(config.digestAlg); } catch (Exception e) { e.printStackTrace(); } 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.printf("zipaligning apk: %s, exists:%b\n", before.getAbsolutePath(), before.exists()); 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() )); } } private void generalUnsignApk(HashMap compressData) throws IOException, InterruptedException { 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("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.println(String.format("add meta file %s", 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.size() == 0) 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"); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/ResourceRepackage.java ================================================ package com.tencent.mm.androlib; 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.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.util.ArrayList; import java.util.HashMap; public class ResourceRepackage { private final String zipalignPath; private final String sevenZipPath; private File mSignedApk; private File mSignedWith7ZipApk; private File mAlignedWith7ZipApk; private File m7zipOutPutDir; private File mStoredOutPutDir; private String mApkName; private File mOutDir; public ResourceRepackage(String zipalignPath, String zipPath, File signedFile) { this.zipalignPath = zipalignPath; this.sevenZipPath = zipPath; mSignedApk = signedFile; } public void setOutDir(File outDir) { mOutDir = outDir; } public void repackageApk() throws IOException, InterruptedException { insureFileName(); repackageWith7z(); alignApk(); deleteUnusedFiles(); } private void deleteUnusedFiles() { //删除目录 FileOperation.deleteDir(m7zipOutPutDir); FileOperation.deleteDir(mStoredOutPutDir); if (mSignedWith7ZipApk.exists()) { mSignedWith7ZipApk.delete(); } } /** * 这边有点不太一样,就是当输出目录存在的时候是不会强制删除目录的 * * @throws IOException */ private void insureFileName() throws IOException { if (!mSignedApk.exists()) { throw new IOException(String.format("can not found the signed apk file to repackage" + ", path=%s", mSignedApk.getAbsolutePath() )); } //需要自己安装7zip String apkBasename = mSignedApk.getName(); mApkName = apkBasename.substring(0, apkBasename.indexOf(".apk")); //如果外面设过,就不用设了 if (mOutDir == null) { mOutDir = new File(mSignedApk.getAbsoluteFile().getParent(), mApkName); } mSignedWith7ZipApk = new File(mOutDir.getAbsolutePath(), mApkName + "_channel_7zip.apk"); mAlignedWith7ZipApk = new File(mOutDir.getAbsolutePath(), mApkName + "_channel_7zip_aligned.apk"); m7zipOutPutDir = new File(mOutDir.getAbsolutePath(), TypedValue.OUT_7ZIP_FILE_PATH); mStoredOutPutDir = new File(mOutDir.getAbsolutePath(), "storefiles"); //删除目录,因为之前的方法是把整个输出目录都删除,所以不会有问题,现在不会,所以要单独删 FileOperation.deleteDir(m7zipOutPutDir); FileOperation.deleteDir(mStoredOutPutDir); FileOperation.deleteDir(mSignedWith7ZipApk); FileOperation.deleteDir(mAlignedWith7ZipApk); } private void repackageWith7z() throws IOException, InterruptedException { System.out.printf("use 7zip to repackage: %s, will cost much more time\n", mSignedWith7ZipApk.getName()); HashMap compressData = FileOperation.unZipAPk(mSignedApk.getAbsolutePath(), m7zipOutPutDir.getAbsolutePath() ); //首先一次性生成一个全部都是压缩的安装包 generalRaw7zip(); 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); if (!mSignedWith7ZipApk.exists()) { throw new IOException(String.format( "[repackageWith7z]7z repackage signed apk fail,you must install 7z command line version first, linux: p7zip, window: 7za, path=%s", mSignedWith7ZipApk.getAbsolutePath() )); } } private void generalRaw7zip() throws IOException, InterruptedException { System.out.printf("general the raw 7zip file\n"); String outPath = m7zipOutPutDir.getAbsoluteFile().getAbsolutePath(); String path = outPath + File.separator + "*"; String cmd = Utils.isPresent(sevenZipPath) ? sevenZipPath : TypedValue.COMMAND_7ZIP; ProcessBuilder pb = new ProcessBuilder(cmd, "a", "-tzip", mSignedWith7ZipApk.getAbsolutePath(), path, "-mx9"); Process pro = pb.start(); InputStreamReader ir = new InputStreamReader(pro.getInputStream()); LineNumberReader input = new LineNumberReader(ir); //如果不读会有问题,被阻塞 while (input.readLine() != null) { } //destroy the stream pro.waitFor(); pro.destroy(); } private void addStoredFileIn7Zip(ArrayList storedFiles) throws IOException, InterruptedException { System.out.printf("[addStoredFileIn7Zip]rewrite the stored file into the 7zip, file count:%d\n", storedFiles.size() ); String storedParentName = mStoredOutPutDir.getAbsolutePath() + 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(sevenZipPath) ? sevenZipPath : TypedValue.COMMAND_7ZIP; ProcessBuilder pb = new ProcessBuilder(cmd, "a", "-tzip", mSignedWith7ZipApk.getAbsolutePath(), storedParentName, "-mx0" ); Process pro = pb.start(); InputStreamReader ir = new InputStreamReader(pro.getInputStream()); LineNumberReader input = new LineNumberReader(ir); //如果不读会有问题,被阻塞 while (input.readLine() != null) { } //destroy the stream pro.waitFor(); pro.destroy(); } private void alignApk() throws IOException, InterruptedException { if (mSignedWith7ZipApk.exists()) { alignApk(mSignedWith7ZipApk, mAlignedWith7ZipApk); } } private void alignApk(File before, File after) throws IOException, InterruptedException { System.out.printf("zipaligning apk: %s\n", before.getName()); 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(zipalignPath) ? zipalignPath : TypedValue.COMMAND_ZIPALIGIN; ProcessBuilder pb = new ProcessBuilder(cmd, "4", before.getAbsolutePath(), after.getAbsolutePath()); Process pro = pb.start(); //destroy the stream pro.waitFor(); pro.destroy(); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/res/data/ResID.java ================================================ /** * Copyright 2014 Ryszard Wiśniewski * Copyright 2016 sim sun * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.androlib.res.data; public class ResID { public final int package_; public final int type; public final int entry; public final int id; public ResID(int package_, int type, int entry) { this(package_, type, entry, (package_ << 24) + (type << 16) + entry); } public ResID(int id) { this((id >> 24) & 0xff, (id >> 16) & 0x000000ff, id & 0x0000ffff, id); } public ResID(int package_, int type, int entry, int id) { this.package_ = package_; this.type = type; this.entry = entry; this.id = id; } @Override public String toString() { return String.format("0x%08x", id); } @Override public int hashCode() { int hash = 17; hash = 31 * hash + this.id; return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ResID other = (ResID) obj; if (this.id != other.id) { return false; } return true; } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/res/data/ResPackage.java ================================================ /** * Copyright 2014 Ryszard Wiśniewski * Copyright 2016 sim sun * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.androlib.res.data; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; public class ResPackage { private final String mName; private final Map mSpecNamesReplace; private final Map> mSpecNamesBlock; private boolean mCanProguard = false; public ResPackage(int id, String name) { this.mName = name; mSpecNamesReplace = new LinkedHashMap<>(); mSpecNamesBlock = new LinkedHashMap<>(); } public boolean isCanResguard() { return mCanProguard; } public void setCanResguard(boolean set) { mCanProguard = set; } public boolean hasSpecRepplace(String resID) { return mSpecNamesReplace.containsKey(resID); } public String getSpecRepplace(int resID) { return mSpecNamesReplace.get(resID); } public void putSpecNamesReplace(int resID, String value) { mSpecNamesReplace.put(resID, value); } public void putSpecNamesblock(String specName, String value) { Set values = mSpecNamesBlock.get(specName); if (values == null) { values = new HashSet<>(); mSpecNamesBlock.put(specName, values); } values.add(value); } public Map> getSpecNamesBlock() { return mSpecNamesBlock; } public String getName() { return mName; } @Override public String toString() { return mName; } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/res/data/ResType.java ================================================ /** * Copyright 2014 Ryszard Wiśniewski * Copyright 2016 sim sun * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.androlib.res.data; import com.tencent.mm.androlib.AndrolibException; import java.util.HashSet; public final class ResType { private final String mName; private final ResPackage mPackage; private final HashSet specNames; public ResType(String name, ResPackage package_) { this.mName = name; this.mPackage = package_; specNames = new HashSet<>(); } public String getName() { return mName; } public void putSpecResguardName(String name) throws AndrolibException { if (specNames.contains(name)) { throw new AndrolibException(String.format( "spec proguard name duplicate in a singal type %s, spec name: %s\n", getName(), name )); } specNames.add(name); } @Override public String toString() { return mName; } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/res/decoder/ARSCDecoder.java ================================================ /** * Copyright 2014 Ryszard Wiśniewski * Copyright 2016 sim sun *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.androlib.res.decoder; import com.mindprod.ledatastream.LEDataInputStream; import com.mindprod.ledatastream.LEDataOutputStream; import com.tencent.mm.androlib.AndrolibException; import com.tencent.mm.androlib.ApkDecoder; import com.tencent.mm.androlib.res.data.ResPackage; import com.tencent.mm.androlib.res.data.ResType; import com.tencent.mm.androlib.res.util.StringUtil; import com.tencent.mm.resourceproguard.Configuration; import com.tencent.mm.util.ExtDataInput; import com.tencent.mm.util.ExtDataOutput; import com.tencent.mm.util.FileOperation; import com.tencent.mm.util.Md5Util; import com.tencent.mm.util.TypedValue; import com.tencent.mm.util.Utils; import java.io.BufferedWriter; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.math.BigInteger; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import java.util.regex.Pattern; public class ARSCDecoder { private final static boolean DEBUG = false; private final static short ENTRY_FLAG_COMPLEX = 0x0001; private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName()); private static final int KNOWN_CONFIG_BYTES = 56; public static Map mTableStringsResguard = new LinkedHashMap<>(); public static int mMergeDuplicatedResCount = 0; private final Map mOldFileName; private final Map mCurSpecNameToPos; private final HashSet mShouldResguardTypeSet; private final ApkDecoder mApkDecoder; private ExtDataInput mIn; private ExtDataOutput mOut; private Header mHeader; private StringBlock mTableStrings; private StringBlock mTypeNames; private StringBlock mSpecNames; private ResPackage mPkg; private ResType mType; private ResPackage[] mPkgs; private int[] mPkgsLenghtChange; private int mTableLenghtChange = 0; private int mResId; private int mCurrTypeID = -1; private int mCurEntryID = -1; private int mCurPackageID = -1; private long mMergeDuplicatedResTotalSize = 0L; private ResguardStringBuilder mResguardBuilder; private boolean mShouldResguardForType = false; private Writer mMappingWriter; private Writer mMergeDuplicatedResMappingWriter; private Map> mMergeDuplicatedResInfoData = new HashMap<>(); private ARSCDecoder(InputStream arscStream, ApkDecoder decoder) throws AndrolibException, IOException { mOldFileName = new LinkedHashMap<>(); mCurSpecNameToPos = new LinkedHashMap<>(); mShouldResguardTypeSet = new HashSet<>(); mIn = new ExtDataInput(new LEDataInputStream(arscStream)); mApkDecoder = decoder; proguardFileName(); } private ARSCDecoder(InputStream arscStream, ApkDecoder decoder, ResPackage[] pkgs) throws FileNotFoundException { mOldFileName = new LinkedHashMap<>(); mCurSpecNameToPos = new LinkedHashMap<>(); mShouldResguardTypeSet = new HashSet<>(); mApkDecoder = decoder; mIn = new ExtDataInput(new LEDataInputStream(arscStream)); mOut = new ExtDataOutput(new LEDataOutputStream(new FileOutputStream(mApkDecoder.getOutTempARSCFile(), false))); mPkgs = pkgs; mPkgsLenghtChange = new int[pkgs.length]; } public static ResPackage[] decode(InputStream arscStream, ApkDecoder apkDecoder) throws AndrolibException { try { ARSCDecoder decoder = new ARSCDecoder(arscStream, apkDecoder); ResPackage[] pkgs = decoder.readTable(); return pkgs; } catch (IOException ex) { throw new AndrolibException("Could not decode arsc file", ex); } } public static void write(InputStream arscStream, ApkDecoder decoder, ResPackage[] pkgs) throws AndrolibException { try { ARSCDecoder writer = new ARSCDecoder(arscStream, decoder, pkgs); writer.writeTable(); } catch (IOException ex) { throw new AndrolibException("Could not decode arsc file", ex); } } private void proguardFileName() throws IOException, AndrolibException { mMappingWriter = new BufferedWriter(new FileWriter(mApkDecoder.getResMappingFile(), false)); mMergeDuplicatedResMappingWriter = new BufferedWriter(new FileWriter(mApkDecoder.getMergeDuplicatedResMappingFile(), false)); mMergeDuplicatedResMappingWriter.write("res filter path mapping:\n"); mMergeDuplicatedResMappingWriter.flush(); mResguardBuilder = new ResguardStringBuilder(); mResguardBuilder.reset(null); final Configuration config = mApkDecoder.getConfig(); File rawResFile = mApkDecoder.getRawResFile(); File[] resFiles = rawResFile.listFiles(); // 需要看看哪些类型是要混淆文件路径的 for (File resFile : resFiles) { String raw = resFile.getName(); if (raw.contains("-")) { raw = raw.substring(0, raw.indexOf("-")); } mShouldResguardTypeSet.add(raw); } if (!config.mKeepRoot) { // 需要保持之前的命名方式 if (config.mUseKeepMapping) { HashMap fileMapping = config.mOldFileMapping; List keepFileNames = new ArrayList<>(); // 这里面为了兼容以前,也需要用以前的文件名前缀,即res混淆成什么 String resRoot = TypedValue.RES_FILE_PATH; for (String name : fileMapping.values()) { int dot = name.indexOf("/"); if (dot == -1) { throw new IOException(String.format("the old mapping res file path should be like r/a, yours %s\n", name)); } resRoot = name.substring(0, dot); keepFileNames.add(name.substring(dot + 1)); } // 去掉所有之前保留的命名,为了简单操作,mapping里面有的都去掉 mResguardBuilder.removeStrings(keepFileNames); for (File resFile : resFiles) { String raw = "res" + "/" + resFile.getName(); if (fileMapping.containsKey(raw)) { mOldFileName.put(raw, fileMapping.get(raw)); } else { mOldFileName.put(raw, resRoot + "/" + mResguardBuilder.getReplaceString()); } } } else { for (int i = 0; i < resFiles.length; i++) { // 这里也要用linux的分隔符,如果普通的话,就是r mOldFileName.put("res" + "/" + resFiles[i].getName(), TypedValue.RES_FILE_PATH + "/" + mResguardBuilder.getReplaceString() ); } } generalFileResMapping(); } Utils.cleanDir(mApkDecoder.getOutResFile()); } private ResPackage[] readTable() throws IOException, AndrolibException { nextChunkCheckType(Header.TYPE_TABLE); int packageCount = mIn.readInt(); mTableStrings = StringBlock.read(mIn); ResPackage[] packages = new ResPackage[packageCount]; nextChunk(); for (int i = 0; i < packageCount; i++) { packages[i] = readPackage(); } mMappingWriter.close(); System.out.printf("resources mapping file %s done\n", mApkDecoder.getResMappingFile().getAbsolutePath()); generalFilterEnd(mMergeDuplicatedResCount, mMergeDuplicatedResTotalSize); mMergeDuplicatedResMappingWriter.close(); System.out.printf("resources filter mapping file %s done\n", mApkDecoder.getMergeDuplicatedResMappingFile().getAbsolutePath()); return packages; } private void writeTable() throws IOException, AndrolibException { System.out.printf("writing new resources.arsc \n"); mTableLenghtChange = 0; writeNextChunkCheck(Header.TYPE_TABLE, 0); int packageCount = mIn.readInt(); mOut.writeInt(packageCount); mTableLenghtChange += StringBlock.writeTableNameStringBlock(mIn, mOut, mTableStringsResguard); writeNextChunk(0); if (packageCount != mPkgs.length) { throw new AndrolibException(String.format("writeTable package count is different before %d, now %d", mPkgs.length, packageCount )); } for (int i = 0; i < packageCount; i++) { mCurPackageID = i; writePackage(); } // 最后需要把整个的size重写回去 reWriteTable(); } private void generalFileResMapping() throws IOException { mMappingWriter.write("res path mapping:\n"); for (String raw : mOldFileName.keySet()) { mMappingWriter.write(" " + raw + " -> " + mOldFileName.get(raw)); mMappingWriter.write("\n"); } mMappingWriter.write("\n\n"); mMappingWriter.write("res id mapping:\n"); mMappingWriter.flush(); } private void generalResIDMapping( String packageName, String typename, String specName, String replace) throws IOException { mMappingWriter.write(" " + packageName + ".R." + typename + "." + specName + " -> " + packageName + ".R." + typename + "." + replace); mMappingWriter.write("\n"); mMappingWriter.flush(); } private void generalFilterResIDMapping( String originalFile, String original, String replaceFile, String replace, long fileLen) throws IOException { mMergeDuplicatedResMappingWriter.write(" " + originalFile + " : " + original + " -> " + replaceFile + " : " + replace + " (size:" + getNetFileSizeDescription(fileLen) + ")"); mMergeDuplicatedResMappingWriter.write("\n"); mMergeDuplicatedResMappingWriter.flush(); } private void generalFilterEnd(int count, long totalSize) throws IOException { mMergeDuplicatedResMappingWriter.write( "removed: count(" + count + "), totalSize(" + getNetFileSizeDescription(totalSize) + ")"); mMergeDuplicatedResMappingWriter.flush(); } private static String getNetFileSizeDescription(long size) { StringBuilder bytes = new StringBuilder(); DecimalFormat format = new DecimalFormat("###.0"); if (size >= 1024 * 1024 * 1024) { double i = (size / (1024.0 * 1024.0 * 1024.0)); bytes.append(format.format(i)).append("GB"); } else if (size >= 1024 * 1024) { double i = (size / (1024.0 * 1024.0)); bytes.append(format.format(i)).append("MB"); } else if (size >= 1024) { double i = (size / (1024.0)); bytes.append(format.format(i)).append("KB"); } else { if (size <= 0) { bytes.append("0B"); } else { bytes.append((int) size).append("B"); } } return bytes.toString(); } private void reWriteTable() throws AndrolibException, IOException { mIn = new ExtDataInput(new LEDataInputStream(new FileInputStream(mApkDecoder.getOutTempARSCFile()))); mOut = new ExtDataOutput(new LEDataOutputStream(new FileOutputStream(mApkDecoder.getOutARSCFile(), false))); writeNextChunkCheck(Header.TYPE_TABLE, mTableLenghtChange); int packageCount = mIn.readInt(); mOut.writeInt(packageCount); StringBlock.writeAll(mIn, mOut); for (int i = 0; i < packageCount; i++) { mCurPackageID = i; writeNextChunk(mPkgsLenghtChange[mCurPackageID]); mOut.writeBytes(mIn, mHeader.chunkSize - 8); } mApkDecoder.getOutTempARSCFile().delete(); } private ResPackage readPackage() throws IOException, AndrolibException { checkChunkType(Header.TYPE_PACKAGE); int id = (byte) mIn.readInt(); String name = mIn.readNullEndedString(128, true); System.out.printf("reading packagename %s\n", name); /* typeNameStrings */ mIn.skipInt(); /* typeNameCount */ mIn.skipInt(); /* specNameStrings */ mIn.skipInt(); /* specNameCount */ mIn.skipInt(); mCurrTypeID = -1; mTypeNames = StringBlock.read(mIn); mSpecNames = StringBlock.read(mIn); mResId = id << 24; mPkg = new ResPackage(id, name); // 系统包名不混淆 if (mPkg.getName().equals("android")) { mPkg.setCanResguard(false); } else { mPkg.setCanResguard(true); } nextChunk(); while (mHeader.type == Header.TYPE_LIBRARY) { readLibraryType(); } while (mHeader.type == Header.TYPE_SPEC_TYPE) { readTableTypeSpec(); } return mPkg; } private void writePackage() throws IOException, AndrolibException { checkChunkType(Header.TYPE_PACKAGE); int id = (byte) mIn.readInt(); mOut.writeInt(id); mResId = id << 24; //char_16的,一共256byte mOut.writeBytes(mIn, 256); /* typeNameStrings */ mOut.writeInt(mIn.readInt()); /* typeNameCount */ mOut.writeInt(mIn.readInt()); /* specNameStrings */ mOut.writeInt(mIn.readInt()); /* specNameCount */ mOut.writeInt(mIn.readInt()); StringBlock.writeAll(mIn, mOut); if (mPkgs[mCurPackageID].isCanResguard()) { int specSizeChange = StringBlock.writeSpecNameStringBlock(mIn, mOut, mPkgs[mCurPackageID].getSpecNamesBlock(), mCurSpecNameToPos ); mPkgsLenghtChange[mCurPackageID] += specSizeChange; mTableLenghtChange += specSizeChange; } else { StringBlock.writeAll(mIn, mOut); } writeNextChunk(0); while (mHeader.type == Header.TYPE_LIBRARY) { writeLibraryType(); } while (mHeader.type == Header.TYPE_SPEC_TYPE) { writeTableTypeSpec(); } } /** * 如果是保持mapping的话,需要去掉某部分已经用过的mapping */ private void reduceFromOldMappingFile() { if (mPkg.isCanResguard()) { if (mApkDecoder.getConfig().mUseKeepMapping) { // 判断是否走keepmapping HashMap>> resMapping = mApkDecoder.getConfig().mOldResMapping; String packName = mPkg.getName(); if (resMapping.containsKey(packName)) { HashMap> typeMaps = resMapping.get(packName); String typeName = mType.getName(); if (typeMaps.containsKey(typeName)) { HashMap proguard = typeMaps.get(typeName); // 去掉所有之前保留的命名,为了简单操作,mapping里面有的都去掉 mResguardBuilder.removeStrings(proguard.values()); } } } } } private HashSet getWhiteList(String resType) { final String packName = mPkg.getName(); if (mApkDecoder.getConfig().mWhiteList.containsKey(packName)) { if (mApkDecoder.getConfig().mUseWhiteList) { HashMap> typeMaps = mApkDecoder.getConfig().mWhiteList.get(packName); return typeMaps.get(resType); } } return null; } private void readLibraryType() throws AndrolibException, IOException { checkChunkType(Header.TYPE_LIBRARY); int libraryCount = mIn.readInt(); int packageId; String packageName; for (int i = 0; i < libraryCount; i++) { packageId = mIn.readInt(); packageName = mIn.readNullEndedString(128, true); System.out.printf("Decoding Shared Library (%s), pkgId: %d\n", packageName, packageId); } while (nextChunk().type == Header.TYPE_TYPE) { readTableTypeSpec(); } } private void readTableTypeSpec() throws AndrolibException, IOException { checkChunkType(Header.TYPE_SPEC_TYPE); byte id = mIn.readByte(); mIn.skipBytes(3); int entryCount = mIn.readInt(); mType = new ResType(mTypeNames.getString(id - 1), mPkg); if (DEBUG) { System.out.printf("[ReadTableType] type (%s) id: (%d) curr (%d)\n", mType, id, mCurrTypeID); } // first meet a type of resource if (mCurrTypeID != id) { mCurrTypeID = id; initResGuardBuild(mCurrTypeID); } // 是否混淆文件路径 mShouldResguardForType = isToResguardFile(mTypeNames.getString(id - 1)); // 对,这里是用来描述差异性的!!! mIn.skipBytes(entryCount * 4); mResId = (0xff000000 & mResId) | id << 16; while (nextChunk().type == Header.TYPE_TYPE) { readConfig(); } } private void initResGuardBuild(int resTypeId) { // we need remove string from resguard candidate list if it exists in white list HashSet whiteListPatterns = getWhiteList(mType.getName()); // init resguard builder mResguardBuilder.reset(whiteListPatterns); mResguardBuilder.removeStrings(RawARSCDecoder.getExistTypeSpecNameStrings(resTypeId)); // 如果是保持mapping的话,需要去掉某部分已经用过的mapping reduceFromOldMappingFile(); } private void writeLibraryType() throws AndrolibException, IOException { checkChunkType(Header.TYPE_LIBRARY); int libraryCount = mIn.readInt(); mOut.writeInt(libraryCount); for (int i = 0; i < libraryCount; i++) { mOut.writeInt(mIn.readInt());/*packageId*/ mOut.writeBytes(mIn, 256); /*packageName*/ } writeNextChunk(0); while (mHeader.type == Header.TYPE_TYPE) { writeTableTypeSpec(); } } private void writeTableTypeSpec() throws AndrolibException, IOException { checkChunkType(Header.TYPE_SPEC_TYPE); byte id = mIn.readByte(); mOut.writeByte(id); mResId = (0xff000000 & mResId) | id << 16; mOut.writeBytes(mIn, 3); int entryCount = mIn.readInt(); mOut.writeInt(entryCount); // 对,这里是用来描述差异性的!!! ///* flags */mIn.skipBytes(entryCount * 4); int[] entryOffsets = mIn.readIntArray(entryCount); mOut.writeIntArray(entryOffsets); while (writeNextChunk(0).type == Header.TYPE_TYPE) { writeConfig(); } } private void readConfig() throws IOException, AndrolibException { checkChunkType(Header.TYPE_TYPE); /* typeId */ mIn.skipInt(); int entryCount = mIn.readInt(); int entriesStart = mIn.readInt(); readConfigFlags(); int[] entryOffsets = mIn.readIntArray(entryCount); for (int i = 0; i < entryOffsets.length; i++) { mCurEntryID = i; if (entryOffsets[i] != -1) { mResId = (mResId & 0xffff0000) | i; readEntry(); } } } private void writeConfig() throws IOException, AndrolibException { checkChunkType(Header.TYPE_TYPE); /* typeId */ mOut.writeInt(mIn.readInt()); /* entryCount */ int entryCount = mIn.readInt(); mOut.writeInt(entryCount); /* entriesStart */ mOut.writeInt(mIn.readInt()); writeConfigFlags(); int[] entryOffsets = mIn.readIntArray(entryCount); mOut.writeIntArray(entryOffsets); for (int i = 0; i < entryOffsets.length; i++) { if (entryOffsets[i] != -1) { mResId = (mResId & 0xffff0000) | i; writeEntry(); } } } private void readEntry() throws IOException, AndrolibException { mIn.skipBytes(2); short flags = mIn.readShort(); int specNamesId = mIn.readInt(); if (mPkg.isCanResguard()) { // 混淆过或者已经添加到白名单的都不需要再处理了 if (!mResguardBuilder.isReplaced(mCurEntryID) && !mResguardBuilder.isInWhiteList(mCurEntryID)) { Configuration config = mApkDecoder.getConfig(); boolean isWhiteList = false; if (config.mUseWhiteList) { isWhiteList = dealWithWhiteList(specNamesId, config); } if (!isWhiteList) { dealWithNonWhiteList(specNamesId, config); } } } if ((flags & ENTRY_FLAG_COMPLEX) == 0) { readValue(true, specNamesId); } else { readComplexEntry(false, specNamesId); } } /** * deal with whitelist * * @param specNamesId resource spec name id * @param config {@Configuration} AndResGuard configuration * @return isWhiteList whether this resource is processed by whitelist */ private boolean dealWithWhiteList(int specNamesId, Configuration config) throws AndrolibException { String packName = mPkg.getName(); if (config.mWhiteList.containsKey(packName)) { HashMap> typeMaps = config.mWhiteList.get(packName); String typeName = mType.getName(); if (typeMaps.containsKey(typeName)) { String specName = mSpecNames.get(specNamesId).toString(); HashSet patterns = typeMaps.get(typeName); for (Iterator it = patterns.iterator(); it.hasNext(); ) { Pattern p = it.next(); if (p.matcher(specName).matches()) { if (DEBUG) { System.out.printf("[match] matcher %s ,typeName %s, specName :%s\n", p.pattern(), typeName, specName); } mPkg.putSpecNamesReplace(mResId, specName); mPkg.putSpecNamesblock(specName, specName); mResguardBuilder.setInWhiteList(mCurEntryID); mType.putSpecResguardName(specName); return true; } } } } return false; } private void dealWithNonWhiteList(int specNamesId, Configuration config) throws AndrolibException, IOException { String replaceString = null; boolean keepMapping = false; if (config.mUseKeepMapping) { String packName = mPkg.getName(); if (config.mOldResMapping.containsKey(packName)) { HashMap> typeMaps = config.mOldResMapping.get(packName); String typeName = mType.getName(); if (typeMaps.containsKey(typeName)) { HashMap nameMap = typeMaps.get(typeName); String specName = mSpecNames.get(specNamesId).toString(); if (nameMap.containsKey(specName)) { keepMapping = true; replaceString = nameMap.get(specName); } } } } if (!keepMapping) { replaceString = mResguardBuilder.getReplaceString(); } mResguardBuilder.setInReplaceList(mCurEntryID); if (replaceString == null) { throw new AndrolibException("readEntry replaceString == null"); } generalResIDMapping(mPkg.getName(), mType.getName(), mSpecNames.get(specNamesId).toString(), replaceString); mPkg.putSpecNamesReplace(mResId, replaceString); // arsc name列混淆成固定名字, 减少string pool大小 boolean useFixedName = config.mFixedResName != null && config.mFixedResName.length() > 0; String fixedName = useFixedName ? config.mFixedResName : replaceString; mPkg.putSpecNamesblock(fixedName, replaceString); mType.putSpecResguardName(replaceString); } private void writeEntry() throws IOException, AndrolibException { /* size */ mOut.writeBytes(mIn, 2); short flags = mIn.readShort(); mOut.writeShort(flags); int specNamesId = mIn.readInt(); ResPackage pkg = mPkgs[mCurPackageID]; if (pkg.isCanResguard()) { specNamesId = mCurSpecNameToPos.get(pkg.getSpecRepplace(mResId)); if (specNamesId < 0) { throw new AndrolibException(String.format("writeEntry new specNamesId < 0 %d", specNamesId)); } } mOut.writeInt(specNamesId); if ((flags & ENTRY_FLAG_COMPLEX) == 0) { writeValue(); } else { writeComplexEntry(); } } /** * @param flags whether read direct */ private void readComplexEntry(boolean flags, int specNamesId) throws IOException, AndrolibException { int parent = mIn.readInt(); int count = mIn.readInt(); for (int i = 0; i < count; i++) { mIn.readInt(); readValue(flags, specNamesId); } } private void writeComplexEntry() throws IOException, AndrolibException { mOut.writeInt(mIn.readInt()); int count = mIn.readInt(); mOut.writeInt(count); for (int i = 0; i < count; i++) { mOut.writeInt(mIn.readInt()); writeValue(); } } /** * @param flags whether read direct */ private void readValue(boolean flags, int specNamesId) throws IOException, AndrolibException { /* size */ mIn.skipCheckShort((short) 8); /* zero */ mIn.skipCheckByte((byte) 0); byte type = mIn.readByte(); int data = mIn.readInt(); //这里面有几个限制,一对于string ,id, array我们是知道肯定不用改的,第二看要那个type是否对应有文件路径 if (mPkg.isCanResguard() && flags && type == TypedValue.TYPE_STRING && mShouldResguardForType && mShouldResguardTypeSet.contains(mType.getName())) { if (mTableStringsResguard.get(data) == null) { String raw = mTableStrings.get(data).toString(); if (StringUtil.isBlank(raw) || raw.equalsIgnoreCase("null")) return; String proguard = mPkg.getSpecRepplace(mResId); //这个要写死这个,因为resources.arsc里面就是用这个 int secondSlash = raw.lastIndexOf("/"); if (secondSlash == -1) { throw new AndrolibException(String.format("can not find \\ or raw string in res path = %s", raw)); } String newFilePath = raw.substring(0, secondSlash); if (!mApkDecoder.getConfig().mKeepRoot) { newFilePath = mOldFileName.get(raw.substring(0, secondSlash)); } if (newFilePath == null) { System.err.printf("can not found new res path, raw=%s\n", raw); return; } //同理这里不能用File.separator,因为resources.arsc里面就是用这个 String result = newFilePath + "/" + proguard; int firstDot = raw.indexOf("."); if (firstDot != -1) { result += raw.substring(firstDot); } String compatibaleraw = new String(raw); String compatibaleresult = new String(result); //为了适配window要做一次转换 if (!File.separator.contains("/")) { compatibaleresult = compatibaleresult.replace("/", File.separator); compatibaleraw = compatibaleraw.replace("/", File.separator); } File resRawFile = new File(mApkDecoder.getOutTempDir().getAbsolutePath() + File.separator + compatibaleraw); File resDestFile = new File(mApkDecoder.getOutDir().getAbsolutePath() + File.separator + compatibaleresult); MergeDuplicatedResInfo filterInfo = null; boolean mergeDuplicatedRes = mApkDecoder.getConfig().mMergeDuplicatedRes; if (mergeDuplicatedRes) { filterInfo = mergeDuplicated(resRawFile, resDestFile, compatibaleraw, result); if (filterInfo != null) { resDestFile = new File(filterInfo.filePath); result = filterInfo.fileName; } } //这里用的是linux的分隔符 HashMap compressData = mApkDecoder.getCompressData(); if (compressData.containsKey(raw)) { compressData.put(result, compressData.get(raw)); } else { System.err.printf("can not find the compress dataresFile=%s\n", raw); } if (!resRawFile.exists()) { System.err.printf("can not find res file, you delete it? path: resFile=%s\n", resRawFile.getAbsolutePath()); } else { if (!mergeDuplicatedRes && resDestFile.exists()) { throw new AndrolibException(String.format("res dest file is already found: destFile=%s", resDestFile.getAbsolutePath() )); } if (filterInfo == null) { FileOperation.copyFileUsingStream(resRawFile, resDestFile); } //already copied mApkDecoder.removeCopiedResFile(resRawFile.toPath()); mTableStringsResguard.put(data, result); } } } } /** * resource filtering, filtering duplicate resources, reducing the volume of apk */ private MergeDuplicatedResInfo mergeDuplicated(File resRawFile, File resDestFile, String compatibaleraw, String result) throws IOException { MergeDuplicatedResInfo filterInfo = null; List mergeDuplicatedResInfoList = mMergeDuplicatedResInfoData.get(resRawFile.length()); if (mergeDuplicatedResInfoList != null) { for (MergeDuplicatedResInfo mergeDuplicatedResInfo : mergeDuplicatedResInfoList) { if (mergeDuplicatedResInfo.md5 == null) { mergeDuplicatedResInfo.md5 = Md5Util.getMD5Str(new File(mergeDuplicatedResInfo.filePath)); } String resRawFileMd5 = Md5Util.getMD5Str(resRawFile); if (!resRawFileMd5.isEmpty() && resRawFileMd5.equals(mergeDuplicatedResInfo.md5)) { filterInfo = mergeDuplicatedResInfo; filterInfo.md5 = resRawFileMd5; break; } } } if (filterInfo != null) { generalFilterResIDMapping(compatibaleraw, result, filterInfo.originalName, filterInfo.fileName, resRawFile.length()); mMergeDuplicatedResCount++; mMergeDuplicatedResTotalSize += resRawFile.length(); } else { MergeDuplicatedResInfo info = new MergeDuplicatedResInfo.Builder() .setFileName(result) .setFilePath(resDestFile.getAbsolutePath()) .setOriginalName(compatibaleraw) .create(); info.fileName = result; info.filePath = resDestFile.getAbsolutePath(); info.originalName = compatibaleraw; if (mergeDuplicatedResInfoList == null) { mergeDuplicatedResInfoList = new ArrayList<>(); mMergeDuplicatedResInfoData.put(resRawFile.length(), mergeDuplicatedResInfoList); } mergeDuplicatedResInfoList.add(info); } return filterInfo; } private void writeValue() throws IOException, AndrolibException { /* size */ mOut.writeCheckShort(mIn.readShort(), (short) 8); /* zero */ mOut.writeCheckByte(mIn.readByte(), (byte) 0); byte type = mIn.readByte(); mOut.writeByte(type); int data = mIn.readInt(); mOut.writeInt(data); } private void readConfigFlags() throws IOException, AndrolibException { int size = mIn.readInt(); int read = 28; if (size < 28) { throw new AndrolibException("Config size < 28"); } boolean isInvalid = false; short mcc = mIn.readShort(); short mnc = mIn.readShort(); char[] language = new char[] { (char) mIn.readByte(), (char) mIn.readByte() }; char[] country = new char[] { (char) mIn.readByte(), (char) mIn.readByte() }; byte orientation = mIn.readByte(); byte touchscreen = mIn.readByte(); int density = mIn.readUnsignedShort(); byte keyboard = mIn.readByte(); byte navigation = mIn.readByte(); byte inputFlags = mIn.readByte(); /* inputPad0 */ mIn.skipBytes(1); short screenWidth = mIn.readShort(); short screenHeight = mIn.readShort(); short sdkVersion = mIn.readShort(); /* minorVersion, now must always be 0 */ mIn.skipBytes(2); byte screenLayout = 0; byte uiMode = 0; short smallestScreenWidthDp = 0; if (size >= 32) { screenLayout = mIn.readByte(); uiMode = mIn.readByte(); smallestScreenWidthDp = mIn.readShort(); read = 32; } short screenWidthDp = 0; short screenHeightDp = 0; if (size >= 36) { screenWidthDp = mIn.readShort(); screenHeightDp = mIn.readShort(); read = 36; } char[] localeScript = null; char[] localeVariant = null; if (size >= 48) { localeScript = readScriptOrVariantChar(4).toCharArray(); localeVariant = readScriptOrVariantChar(8).toCharArray(); read = 48; } byte screenLayout2 = 0; if (size >= 52) { screenLayout2 = mIn.readByte(); mIn.skipBytes(3); // reserved padding read = 52; } if (size >= 56) { mIn.skipBytes(4); read = 56; } int exceedingSize = size - KNOWN_CONFIG_BYTES; if (exceedingSize > 0) { byte[] buf = new byte[exceedingSize]; read += exceedingSize; mIn.readFully(buf); BigInteger exceedingBI = new BigInteger(1, buf); if (exceedingBI.equals(BigInteger.ZERO)) { LOGGER.fine(String.format("Config flags size > %d, but exceeding bytes are all zero, so it should be ok.", KNOWN_CONFIG_BYTES )); } else { LOGGER.warning(String.format("Config flags size > %d. Exceeding bytes: 0x%X.", KNOWN_CONFIG_BYTES, exceedingBI )); isInvalid = true; } } } private String readScriptOrVariantChar(int length) throws AndrolibException, IOException { StringBuilder string = new StringBuilder(16); while (length-- != 0) { short ch = mIn.readByte(); if (ch == 0) { break; } string.append((char) ch); } mIn.skipBytes(length); return string.toString(); } private void writeConfigFlags() throws IOException, AndrolibException { //总的有多大 int size = mIn.readInt(); if (size < 28) { throw new AndrolibException("Config size < 28"); } mOut.writeInt(size); mOut.writeBytes(mIn, size - 4); } private Header nextChunk() throws IOException { return mHeader = Header.read(mIn); } private void checkChunkType(int expectedType) throws AndrolibException { if (mHeader.type != expectedType) { throw new AndrolibException(String.format("Invalid chunk type: expected=0x%08x, got=0x%08x", expectedType, mHeader.type )); } } private void nextChunkCheckType(int expectedType) throws IOException, AndrolibException { nextChunk(); checkChunkType(expectedType); } private Header writeNextChunk(int diffSize) throws IOException, AndrolibException { mHeader = Header.readAndWriteHeader(mIn, mOut, diffSize); return mHeader; } private Header writeNextChunkCheck(int expectedType, int diffSize) throws IOException, AndrolibException { mHeader = Header.readAndWriteHeader(mIn, mOut, diffSize); if (mHeader.type != expectedType) { throw new AndrolibException(String.format("Invalid chunk type: expected=%d, got=%d", expectedType, mHeader.type)); } return mHeader; } /** * 为了加速,不需要处理string,id,array,这几个是肯定不是的 */ private boolean isToResguardFile(String name) { return (!name.equals("string") && !name.equals("id") && !name.equals("array")); } public static class Header { public final static short TYPE_NONE = -1, TYPE_TABLE = 0x0002, TYPE_PACKAGE = 0x0200, TYPE_TYPE = 0x0201, TYPE_SPEC_TYPE = 0x0202, TYPE_LIBRARY = 0x0203; public final short type; public final int chunkSize; public Header(short type, int size) { this.type = type; this.chunkSize = size; } public static Header read(ExtDataInput in) throws IOException { short type; try { type = in.readShort(); short count = in.readShort(); int size = in.readInt(); return new Header(type, size); } catch (EOFException ex) { return new Header(TYPE_NONE, 0); } } public static Header readAndWriteHeader(ExtDataInput in, ExtDataOutput out, int diffSize) throws IOException, AndrolibException { short type; int size; try { type = in.readShort(); out.writeShort(type); short count = in.readShort(); out.writeShort(count); size = in.readInt(); size -= diffSize; if (size <= 0) { throw new AndrolibException(String.format("readAndWriteHeader size < 0: size=%d", size)); } out.writeInt(size); } catch (EOFException ex) { return new Header(TYPE_NONE, 0); } return new Header(type, size); } } public static class FlagsOffset { public final int offset; public final int count; public FlagsOffset(int offset, int count) { this.offset = offset; this.count = count; } } private static class MergeDuplicatedResInfo { private String fileName; private String filePath; private String originalName; private String md5; private MergeDuplicatedResInfo(String fileName, String filePath, String originalName, String md5) { this.fileName = fileName; this.filePath = filePath; this.originalName = originalName; this.md5 = md5; } static class Builder { private String fileName; private String filePath; private String originalName; private String md5; Builder setFileName(String fileName) { this.fileName = fileName; return this; } Builder setFilePath(String filePath) { this.filePath = filePath; return this; } public Builder setMd5(String md5) { this.md5 = md5; return this; } Builder setOriginalName(String originalName) { this.originalName = originalName; return this; } MergeDuplicatedResInfo create() { return new MergeDuplicatedResInfo(fileName, filePath, originalName, md5); } } } private class ResguardStringBuilder { private final List mReplaceStringBuffer; private final Set mIsReplaced; private final Set mIsWhiteList; private String[] mAToZ = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" }; private String[] mAToAll = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "_", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" }; /** * 在window上面有些关键字是不能作为文件名的 * CON, PRN, AUX, CLOCK$, NUL * COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9 * LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, and LPT9. */ private HashSet mFileNameBlackList; public ResguardStringBuilder() { mFileNameBlackList = new HashSet<>(); mFileNameBlackList.add("con"); mFileNameBlackList.add("prn"); mFileNameBlackList.add("aux"); mFileNameBlackList.add("nul"); mReplaceStringBuffer = new ArrayList<>(); mIsReplaced = new HashSet<>(); mIsWhiteList = new HashSet<>(); } public void reset(HashSet blacklistPatterns) { mReplaceStringBuffer.clear(); mIsReplaced.clear(); mIsWhiteList.clear(); for (int i = 0; i < mAToZ.length; i++) { String str = mAToZ[i]; if (!Utils.match(str, blacklistPatterns)) { mReplaceStringBuffer.add(str); } } for (int i = 0; i < mAToZ.length; i++) { String first = mAToZ[i]; for (int j = 0; j < mAToAll.length; j++) { String str = first + mAToAll[j]; if (!Utils.match(str, blacklistPatterns)) { mReplaceStringBuffer.add(str); } } } for (int i = 0; i < mAToZ.length; i++) { String first = mAToZ[i]; for (int j = 0; j < mAToAll.length; j++) { String second = mAToAll[j]; for (int k = 0; k < mAToAll.length; k++) { String third = mAToAll[k]; String str = first + second + third; if (!mFileNameBlackList.contains(str) && !Utils.match(str, blacklistPatterns)) { mReplaceStringBuffer.add(str); } } } } } // 对于某种类型用过的mapping,全部不能再用了 public void removeStrings(Collection collection) { if (collection == null) return; mReplaceStringBuffer.removeAll(collection); } public boolean isReplaced(int id) { return mIsReplaced.contains(id); } public boolean isInWhiteList(int id) { return mIsWhiteList.contains(id); } public void setInWhiteList(int id) { mIsWhiteList.add(id); } public void setInReplaceList(int id) { mIsReplaced.add(id); } public String getReplaceString() throws AndrolibException { if (mReplaceStringBuffer.isEmpty()) { throw new AndrolibException(String.format("now can only proguard less than 35594 in a single type\n")); } return mReplaceStringBuffer.remove(0); } } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/res/decoder/RawARSCDecoder.java ================================================ /** * Copyright 2014 Ryszard Wiśniewski * Copyright 2016 sim sun * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.androlib.res.decoder; import com.mindprod.ledatastream.LEDataInputStream; import com.tencent.mm.androlib.AndrolibException; import com.tencent.mm.androlib.res.data.ResPackage; import com.tencent.mm.androlib.res.data.ResType; import com.tencent.mm.util.ExtDataInput; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.logging.Logger; import org.apache.commons.io.input.CountingInputStream; /** * 其实应该是原来有,并且在白名单里面的才去掉!现在没有判断是否在白名单中 * * @author shwenzhang */ public class RawARSCDecoder { private final static short ENTRY_FLAG_COMPLEX = 0x0001; private final static short ENTRY_FLAG_PUBLIC = 0x0002; private final static short ENTRY_FLAG_WEAK = 0x0004; private static final Logger LOGGER = Logger.getLogger(ARSCDecoder.class.getName()); private static final int KNOWN_CONFIG_BYTES = 64; private static HashMap> mExistTypeNames; private final CountingInputStream mCountIn; private ExtDataInput mIn; private Header mHeader; private StringBlock mTypeNames; private StringBlock mSpecNames; private ResPackage mPkg; private ResType mType; private int mTypeIdOffset = 0; private int mCurTypeID = -1; private ResPackage[] mPkgs; private int mResId; private RawARSCDecoder(InputStream arscStream) throws AndrolibException, IOException { arscStream = mCountIn = new CountingInputStream(arscStream); mIn = new ExtDataInput(new LEDataInputStream(arscStream)); mExistTypeNames = new HashMap<>(); } public static ResPackage[] decode(InputStream arscStream) throws AndrolibException { try { RawARSCDecoder decoder = new RawARSCDecoder(arscStream); System.out.printf("parse to get the exist names in the resouces.arsc first\n"); return decoder.readTable(); } catch (IOException ex) { throw new AndrolibException("Could not decode arsc file", ex); } } public static Set getExistTypeSpecNameStrings(int type) { return mExistTypeNames.get(type); } private ResPackage[] readTable() throws IOException, AndrolibException { nextChunkCheckType(Header.TYPE_TABLE); int packageCount = mIn.readInt(); StringBlock.read(mIn); ResPackage[] packages = new ResPackage[packageCount]; nextChunk(); for (int i = 0; i < packageCount; i++) { packages[i] = readTablePackage(); } return packages; } private ResPackage readTablePackage() throws IOException, AndrolibException { checkChunkType(Header.TYPE_PACKAGE); int id = mIn.readInt(); String name = mIn.readNullEndedString(128, true); /* typeNameStrings */ mIn.skipInt(); /* typeNameCount */ mIn.skipInt(); /* specNameStrings */ mIn.skipInt(); /* specNameCount */ mIn.skipInt(); // TypeIdOffset was added platform_frameworks_base/@f90f2f8dc36e7243b85e0b6a7fd5a590893c827e // which is only in split/new applications. int splitHeaderSize = (2 + 2 + 4 + 4 + (2 * 128) + (4 * 5)); // short, short, int, int, char[128], int * 4 if (mHeader.headerSize == splitHeaderSize) { mTypeIdOffset = mIn.readInt(); } mTypeNames = StringBlock.read(mIn); mSpecNames = StringBlock.read(mIn); mResId = id << 24; mPkg = new ResPackage(id, name); nextChunk(); while (mHeader.type == Header.TYPE_LIBRARY) { readLibraryType(); } while (mHeader.type == Header.TYPE_SPEC_TYPE) { readTableTypeSpec(); } return mPkg; } private void readLibraryType() throws AndrolibException, IOException { checkChunkType(Header.TYPE_LIBRARY); int libraryCount = mIn.readInt(); int packageId; String packageName; for (int i = 0; i < libraryCount; i++) { packageId = mIn.readInt(); packageName = mIn.readNullEndedString(128, true); System.out.printf("Decoding Shared Library (%s), pkgId: %d\n", packageName, packageId); } nextChunk(); while (mHeader.type == Header.TYPE_TYPE) { readTableTypeSpec(); } } private void readTableTypeSpec() throws AndrolibException, IOException { readSingleTableTypeSpec(); nextChunk(); while (mHeader.type == Header.TYPE_SPEC_TYPE) { readSingleTableTypeSpec(); nextChunk(); } while (mHeader.type == Header.TYPE_TYPE) { readConfig(); nextChunk(); } } private void readSingleTableTypeSpec() throws AndrolibException, IOException { checkChunkType(Header.TYPE_SPEC_TYPE); int id = mIn.readUnsignedByte(); mIn.skipBytes(3); int entryCount = mIn.readInt(); /* flags */ mIn.skipBytes(entryCount * 4); mCurTypeID = id; mResId = (0xff000000 & mResId) | id << 16; mType = new ResType(mTypeNames.getString(id - 1), mPkg); } private void readConfig() throws IOException, AndrolibException { checkChunkType(Header.TYPE_TYPE); int typeId = mIn.readUnsignedByte() - mTypeIdOffset; int typeFlags = mIn.readByte(); /* reserved */ mIn.skipBytes(2); int entryCount = mIn.readInt(); int entriesStart = mIn.readInt(); readConfigFlags(); int[] entryOffsets = mIn.readIntArray(entryCount); for (int i = 0; i < entryOffsets.length; i++) { if (entryOffsets[i] != -1) { mResId = (mResId & 0xffff0000) | i; readEntry(); } } } /** * 需要防止由于某些非常恶心的白名单,导致出现重复id * * @throws IOException * @throws AndrolibException */ private void readEntry() throws IOException, AndrolibException { /* size */ mIn.skipBytes(2); short flags = mIn.readShort(); int specNamesId = mIn.readInt(); putTypeSpecNameStrings(mCurTypeID, mSpecNames.getString(specNamesId)); boolean readDirect = false; if ((flags & ENTRY_FLAG_COMPLEX) == 0) { readDirect = true; readValue(readDirect, specNamesId); } else { readDirect = false; readComplexEntry(readDirect, specNamesId); } } private void readComplexEntry(boolean flags, int specNamesId) throws IOException, AndrolibException { int parent = mIn.readInt(); int count = mIn.readInt(); for (int i = 0; i < count; i++) { mIn.readInt(); readValue(flags, specNamesId); } } private void readValue(boolean flags, int specNamesId) throws IOException, AndrolibException { /* size */ mIn.skipCheckShort((short) 8); /* zero */ mIn.skipCheckByte((byte) 0); byte type = mIn.readByte(); int data = mIn.readInt(); } private void readConfigFlags() throws IOException, AndrolibException { int read = 28; int size = mIn.readInt(); if (size < 28) { throw new AndrolibException("Config size < 28"); } boolean isInvalid = false; short mcc = mIn.readShort(); short mnc = mIn.readShort(); char[] language = new char[] { (char) mIn.readByte(), (char) mIn.readByte() }; char[] country = new char[] { (char) mIn.readByte(), (char) mIn.readByte() }; byte orientation = mIn.readByte(); byte touchscreen = mIn.readByte(); int density = mIn.readUnsignedShort(); byte keyboard = mIn.readByte(); byte navigation = mIn.readByte(); byte inputFlags = mIn.readByte(); /* inputPad0 */ mIn.skipBytes(1); short screenWidth = mIn.readShort(); short screenHeight = mIn.readShort(); short sdkVersion = mIn.readShort(); /* minorVersion, now must always be 0 */ mIn.skipBytes(2); byte screenLayout = 0; byte uiMode = 0; short smallestScreenWidthDp = 0; if (size >= 32) { screenLayout = mIn.readByte(); uiMode = mIn.readByte(); smallestScreenWidthDp = mIn.readShort(); read = 32; } short screenWidthDp = 0; short screenHeightDp = 0; if (size >= 36) { screenWidthDp = mIn.readShort(); screenHeightDp = mIn.readShort(); read = 36; } char[] localeScript = null; char[] localeVariant = null; if (size >= 48) { localeScript = readScriptOrVariantChar(4).toCharArray(); localeVariant = readScriptOrVariantChar(8).toCharArray(); read = 48; } byte screenLayout2 = 0; if (size >= 52) { screenLayout2 = mIn.readByte(); mIn.skipBytes(3); // reserved padding read = 52; } if (size >= 56) { mIn.skipBytes(4); read = 56; } if (size >= 64) { mIn.skipBytes(8); read = 64; } int exceedingSize = size - KNOWN_CONFIG_BYTES; if (exceedingSize > 0) { byte[] buf = new byte[exceedingSize]; mIn.readFully(buf); BigInteger exceedingBI = new BigInteger(1, buf); if (exceedingBI.equals(BigInteger.ZERO)) { LOGGER.fine(String.format("Config flags size > %d, but exceeding bytes are all zero, so it should be ok.", KNOWN_CONFIG_BYTES )); } else { LOGGER.warning(String.format("Config flags size > %d. Exceeding bytes: 0x%X.", KNOWN_CONFIG_BYTES, exceedingBI )); } } else { int remainingSize = size - read; if (remainingSize > 0) { mIn.skipBytes(remainingSize); } } } private String readScriptOrVariantChar(int length) throws AndrolibException, IOException { StringBuilder string = new StringBuilder(16); while (length-- != 0) { short ch = mIn.readByte(); if (ch == 0) { break; } string.append((char) ch); } mIn.skipBytes(length); return string.toString(); } private Header nextChunk() throws IOException { return mHeader = Header.read(mIn, mCountIn); } private void checkChunkType(int expectedType) throws AndrolibException { if (mHeader.type != expectedType) { throw new AndrolibException(String.format("Invalid chunk type: expected=0x%08x, got=0x%08x", expectedType, mHeader.type )); } } private void nextChunkCheckType(int expectedType) throws IOException, AndrolibException { nextChunk(); checkChunkType(expectedType); } private void putTypeSpecNameStrings(int type, String name) { Set names = mExistTypeNames.get(type); if (names == null) { names = new HashSet<>(); } names.add(name); mExistTypeNames.put(type, names); } public static class Header { public final static short TYPE_NONE = -1; public final static short TYPE_TABLE = 0x0002; public final static short TYPE_PACKAGE = 0x0200; public final static short TYPE_TYPE = 0x0201; public final static short TYPE_SPEC_TYPE = 0x0202; public final static short TYPE_LIBRARY = 0x0203; public final short type; public final int headerSize; public final int chunkSize; public final int startPosition; public final int endPosition; public Header(short type, int headerSize, int chunkSize, int headerStart) { this.type = type; this.headerSize = headerSize; this.chunkSize = chunkSize; this.startPosition = headerStart; this.endPosition = headerStart + chunkSize; } public static Header read(ExtDataInput in, CountingInputStream countIn) throws IOException { short type; int start = countIn.getCount(); try { type = in.readShort(); } catch (EOFException ex) { return new Header(TYPE_NONE, 0, 0, countIn.getCount()); } return new Header(type, in.readShort(), in.readInt(), start); } } public static class FlagsOffset { public final int offset; public final int count; public FlagsOffset(int offset, int count) { this.offset = offset; this.count = count; } } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/res/decoder/StringBlock.java ================================================ /** * Copyright 2014 Ryszard Wiśniewski * Copyright 2016 sim sun * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.androlib.res.decoder; import com.tencent.mm.androlib.AndrolibException; import com.tencent.mm.util.ExtDataInput; import com.tencent.mm.util.ExtDataOutput; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.util.Arrays; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** * @author shwenzhang */ public class StringBlock { private static final CharsetDecoder UTF16LE_DECODER = Charset.forName("UTF-16LE").newDecoder(); private static final CharsetDecoder UTF8_DECODER = Charset.forName("UTF-8").newDecoder(); private static final Logger LOGGER = Logger.getLogger(StringBlock.class.getName()); // ResChunk_header = header.type (0x0001) + header.headerSize (0x001C) private static final int CHUNK_STRINGPOOL_TYPE = 0x001C0001; private static final int UTF8_FLAG = 0x00000100; private static final int CHUNK_NULL_TYPE = 0x00000000; private static final byte NULL = 0; private int[] m_stringOffsets; private byte[] m_strings; private int[] m_styleOffsets; private int[] m_styles; private boolean m_isUTF8; private int[] m_stringOwns; private StringBlock() { } /** * Reads whole (including chunk type) string block from stream. Stream must * be at the chunk type. * * @param reader reader * @return stringblock * @throws IOException ioexcetpion */ public static StringBlock read(ExtDataInput reader) throws IOException { reader.skipCheckChunkTypeInt(CHUNK_STRINGPOOL_TYPE, CHUNK_NULL_TYPE); int chunkSize = reader.readInt(); int stringCount = reader.readInt(); int styleCount = reader.readInt(); int flags = reader.readInt(); int stringsOffset = reader.readInt(); int stylesOffset = reader.readInt(); StringBlock block = new StringBlock(); block.m_isUTF8 = (flags & UTF8_FLAG) != 0; block.m_stringOffsets = reader.readIntArray(stringCount); block.m_stringOwns = new int[stringCount]; Arrays.fill(block.m_stringOwns, -1); if (styleCount != 0) { block.m_styleOffsets = reader.readIntArray(styleCount); } { int size = ((stylesOffset == 0) ? chunkSize : stylesOffset) - stringsOffset; if ((size % 4) != 0) { throw new IOException("String data size is not multiple of 4 (" + size + ")."); } block.m_strings = new byte[size]; reader.readFully(block.m_strings); } if (stylesOffset != 0) { int size = (chunkSize - stylesOffset); if ((size % 4) != 0) { throw new IOException("Style data size is not multiple of 4 (" + size + ")."); } block.m_styles = reader.readIntArray(size / 4); } return block; } public static int writeSpecNameStringBlock( ExtDataInput reader, ExtDataOutput out, Map> specNames, Map curSpecNameToPos) throws IOException, AndrolibException { int type = reader.readInt(); int chunkSize = reader.readInt(); int stringCount = reader.readInt(); int styleOffsetCount = reader.readInt(); if (styleOffsetCount != 0) { throw new AndrolibException(String.format("writeSpecNameStringBlock styleOffsetCount != 0 styleOffsetCount %d", styleOffsetCount )); } int flags = reader.readInt(); boolean isUTF8 = (flags & UTF8_FLAG) != 0; int stringsOffset = reader.readInt(); int stylesOffset = reader.readInt(); reader.readIntArray(stringCount); int size = ((stylesOffset == 0) ? chunkSize : stylesOffset) - stringsOffset; if ((size % 4) != 0) { throw new IOException("String data size is not multiple of 4 (" + size + ")."); } byte[] temp_strings = new byte[size]; reader.readFully(temp_strings); int totalSize = 0; out.writeCheckInt(type, CHUNK_STRINGPOOL_TYPE); totalSize += 4; stringCount = specNames.keySet().size(); System.out.println("String pool size: " + stringCount); totalSize += 6 * 4 + 4 * stringCount; stringsOffset = totalSize; int[] stringOffsets = new int[stringCount]; // make twice size buffer for avoiding out of bounds error byte[] stringBytes = new byte[size * 2]; int offset = 0; int i = 0; curSpecNameToPos.clear(); for (Iterator it = specNames.keySet().iterator(); it.hasNext(); ) { stringOffsets[i] = offset; String name = it.next(); for (String specName : specNames.get(name)) { // N res entry item point to one string constant curSpecNameToPos.put(specName, i); } if (isUTF8) { stringBytes[offset++] = (byte) name.length(); stringBytes[offset++] = (byte) name.length(); totalSize += 2; byte[] tempByte = name.getBytes(Charset.forName("UTF-8")); if (name.length() != tempByte.length) { throw new AndrolibException(String.format( "writeSpecNameStringBlock %s UTF-8 length is different name %d, tempByte %d\n", name, name.length(), tempByte.length )); } System.arraycopy(tempByte, 0, stringBytes, offset, tempByte.length); offset += name.length(); stringBytes[offset++] = NULL; totalSize += name.length() + 1; } else { writeShort(stringBytes, offset, (short) name.length()); offset += 2; totalSize += 2; byte[] tempByte = name.getBytes(Charset.forName("UTF-16LE")); if ((name.length() * 2) != tempByte.length) { throw new AndrolibException(String.format( "writeSpecNameStringBlock %s UTF-16LE length is different name %d, tempByte %d\n", name, name.length(), tempByte.length )); } System.arraycopy(tempByte, 0, stringBytes, offset, tempByte.length); offset += tempByte.length; stringBytes[offset++] = NULL; stringBytes[offset++] = NULL; totalSize += tempByte.length + 2; } i++; } //要保证string size 是4的倍数,要补零 size = totalSize - stringsOffset; if ((size % 4) != 0) { int add = 4 - (size % 4); for (i = 0; i < add; i++) { stringBytes[offset++] = NULL; totalSize++; } } out.writeInt(totalSize); out.writeInt(stringCount); out.writeInt(styleOffsetCount); out.writeInt(flags); out.writeInt(stringsOffset); out.writeInt(stylesOffset); out.writeIntArray(stringOffsets); out.write(stringBytes, 0, offset); return (chunkSize - totalSize); } public static int writeTableNameStringBlock( ExtDataInput reader, ExtDataOutput out, Map tableProguardMap) throws IOException, AndrolibException { int type = reader.readInt(); int chunkSize = reader.readInt(); int stringCount = reader.readInt(); int styleOffsetCount = reader.readInt(); int flags = reader.readInt(); int stringsOffset = reader.readInt(); int stylesOffset = reader.readInt(); StringBlock block = new StringBlock(); block.m_isUTF8 = (flags & UTF8_FLAG) != 0; if (block.m_isUTF8) { System.out.printf("resources.arsc Character Encoding: utf-8\n"); } else { System.out.printf("resources.arsc Character Encoding: utf-16\n"); } block.m_stringOffsets = reader.readIntArray(stringCount); block.m_stringOwns = new int[stringCount]; for (int i = 0; i < stringCount; i++) { block.m_stringOwns[i] = -1; } if (styleOffsetCount != 0) { block.m_styleOffsets = reader.readIntArray(styleOffsetCount); } { int size = ((stylesOffset == 0) ? chunkSize : stylesOffset) - stringsOffset; if ((size % 4) != 0) { throw new IOException("String data size is not multiple of 4 (" + size + ")."); } block.m_strings = new byte[size]; reader.readFully(block.m_strings); } if (stylesOffset != 0) { int size = (chunkSize - stylesOffset); if ((size % 4) != 0) { throw new IOException("Style data size is not multiple of 4 (" + size + ")."); } block.m_styles = reader.readIntArray(size / 4); } int totalSize = 0; out.writeCheckInt(type, CHUNK_STRINGPOOL_TYPE); totalSize += 4; totalSize += 6 * 4 + 4 * stringCount + 4 * styleOffsetCount; stringsOffset = totalSize; byte[] strings = new byte[block.m_strings.length]; int[] stringOffsets = new int[stringCount]; System.arraycopy(block.m_stringOffsets, 0, stringOffsets, 0, stringOffsets.length); int offset = 0; int i; for (i = 0; i < stringCount; i++) { stringOffsets[i] = offset; //如果找不到即没混淆这一项,直接拷贝 if (tableProguardMap.get(i) == null) { //需要区分是否是最后一项 int copyLen = (i == (stringCount - 1)) ? (block.m_strings.length - block.m_stringOffsets[i]) : (block.m_stringOffsets[i + 1] - block.m_stringOffsets[i]); System.arraycopy(block.m_strings, block.m_stringOffsets[i], strings, offset, copyLen); offset += copyLen; totalSize += copyLen; } else { String name = tableProguardMap.get(i); if (block.m_isUTF8) { strings[offset++] = (byte) name.length(); strings[offset++] = (byte) name.length(); totalSize += 2; byte[] tempByte = name.getBytes(Charset.forName("UTF-8")); if (name.length() != tempByte.length) { throw new AndrolibException(String.format( "writeTableNameStringBlock UTF-8 length is different name %d, tempByte %d\n", name.length(), tempByte.length )); } System.arraycopy(tempByte, 0, strings, offset, tempByte.length); offset += name.length(); strings[offset++] = NULL; totalSize += name.length() + 1; } else { writeShort(strings, offset, (short) name.length()); offset += 2; totalSize += 2; byte[] tempByte = name.getBytes(Charset.forName("UTF-16LE")); if ((name.length() * 2) != tempByte.length) { throw new AndrolibException(String.format( "writeTableNameStringBlock UTF-16LE length is different name %d, tempByte %d\n", name.length(), tempByte.length )); } System.arraycopy(tempByte, 0, strings, offset, tempByte.length); offset += tempByte.length; strings[offset++] = NULL; strings[offset++] = NULL; totalSize += tempByte.length + 2; } } } //要保证string size 是4的倍数,要补零 int size = totalSize - stringsOffset; if ((size % 4) != 0) { int add = 4 - (size % 4); for (i = 0; i < add; i++) { strings[offset++] = NULL; totalSize++; } } //因为是int的,如果之前的不为0 if (stylesOffset != 0) { stylesOffset = totalSize; totalSize += block.m_styles.length * 4; } out.writeInt(totalSize); out.writeInt(stringCount); out.writeInt(styleOffsetCount); out.writeInt(flags); out.writeInt(stringsOffset); out.writeInt(stylesOffset); out.writeIntArray(stringOffsets); if (stylesOffset != 0) { out.writeIntArray(block.m_styleOffsets); } out.write(strings, 0, offset); if (stylesOffset != 0) { out.writeIntArray(block.m_styles); } return (chunkSize - totalSize); } /** * Reads whole (including chunk type) string block from stream. Stream must * be at the chunk type. * * @param reader ExtDataInput reader * @param out ExtDataOutput out * @throws IOException ioexception */ public static void writeAll(ExtDataInput reader, ExtDataOutput out) throws IOException { out.writeCheckChunkTypeInt(reader, CHUNK_STRINGPOOL_TYPE, CHUNK_NULL_TYPE); int chunkSize = reader.readInt(); out.writeInt(chunkSize); out.writeBytes(reader, chunkSize - 8); } private static final int[] getUtf8(byte[] array, int offset) { int val = array[offset]; int length; // We skip the utf16 length of the string if ((val & 0x80) != 0) { offset += 2; } else { offset += 1; } // And we read only the utf-8 encoded length of the string val = array[offset]; offset += 1; if ((val & 0x80) != 0) { int low = (array[offset] & 0xFF); length = ((val & 0x7F) << 8) + low; offset += 1; } else { length = val; } return new int[] { offset, length }; } private static final int[] getUtf16(byte[] array, int offset) { int val = ((array[offset + 1] & 0xFF) << 8 | array[offset] & 0xFF); if ((val & 0x8000) != 0) { int high = (array[offset + 3] & 0xFF) << 8; int low = (array[offset + 2] & 0xFF); int len_value = ((val & 0x7FFF) << 16) + (high + low); return new int[] { 4, len_value * 2 }; } return new int[] { 2, val * 2 }; } private static final int getShort(byte[] array, int offset) { return (array[offset + 1] & 0xff) << 8 | array[offset] & 0xff; } private static final void writeShort(byte[] array, int offset, short value) { array[offset] = (byte) (0xFF & value); array[offset + 1] = (byte) (0xFF & (value >> 8)); } private static final int getShort(int[] array, int offset) { int value = array[offset / 4]; if ((offset % 4) / 2 == 0) { return (value & 0xFFFF); } else { return (value >>> 16); } } /** * Returns number of strings in block. * * @return int number of strings in block. */ public int getCount() { return m_stringOffsets != null ? m_stringOffsets.length : 0; } /** * Returns raw string (without any styling information) at specified index. * * @param index index * @return raw string */ public String getString(int index) { if (index < 0 || m_stringOffsets == null || index >= m_stringOffsets.length) { return null; } int offset = m_stringOffsets[index]; int length; if (m_isUTF8) { int[] val = getUtf8(m_strings, offset); offset = val[0]; length = val[1]; } else { int[] val = getUtf16(m_strings, offset); offset += val[0]; length = val[1]; } return decodeString(offset, length); } /** * Not yet implemented. *

* Returns string with style information (if any). * * @param index index * @return string with style information (if any). */ public CharSequence get(int index) { return getString(index); } /** * Finds index of the string. Returns -1 if the string was not found. * * @param string input string * @return index of the string */ public int find(String string) { if (string == null) { return -1; } for (int i = 0; i != m_stringOffsets.length; ++i) { int offset = m_stringOffsets[i]; int length = getShort(m_strings, offset); if (length != string.length()) { continue; } int j = 0; for (; j != length; ++j) { offset += 2; if (string.charAt(j) != getShort(m_strings, offset)) { break; } } if (j == length) { return i; } } return -1; } private String decodeString(int offset, int length) { try { return (m_isUTF8 ? UTF8_DECODER : UTF16LE_DECODER).decode(ByteBuffer.wrap(m_strings, offset, length)).toString(); } catch (CharacterCodingException ex) { LOGGER.log(Level.WARNING, null, ex); return null; } } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/res/util/ExtFile.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.androlib.res.util; import com.tencent.mm.directory.Directory; import com.tencent.mm.directory.DirectoryException; import com.tencent.mm.directory.FileDirectory; import com.tencent.mm.directory.ZipRODirectory; import java.io.File; import java.net.URI; public class ExtFile extends File { private Directory mDirectory; public ExtFile(File file) { super(file.getPath()); } public ExtFile(URI uri) { super(uri); } public ExtFile(File parent, String child) { super(parent, child); } public ExtFile(String parent, String child) { super(parent, child); } public ExtFile(String pathname) { super(pathname); } public Directory getDirectory() throws DirectoryException { if (mDirectory == null) { if (isDirectory()) { mDirectory = new FileDirectory(this); } else { mDirectory = new ZipRODirectory(this); } } return mDirectory; } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/androlib/res/util/StringUtil.java ================================================ package com.tencent.mm.androlib.res.util; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; public class StringUtil { public static boolean isPresent(final String string) { return string != null && string.length() > 0; } public static boolean isBlank(final String string) { return !isPresent(string); } public static String readInputStream(InputStream inputStream) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int length; while ((length = inputStream.read(buffer)) != -1) { result.write(buffer, 0, length); } return result.toString("UTF-8"); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/directory/AbstractDirectory.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.directory; import java.io.InputStream; import java.io.OutputStream; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; public abstract class AbstractDirectory implements Directory { protected Set mFiles; protected Map mDirs; @Override public Set getFiles() { return getFiles(false); } @Override public Set getFiles(boolean recursive) { if (mFiles == null) { loadFiles(); } if (!recursive) { return mFiles; } Set files = new LinkedHashSet(mFiles); for (Map.Entry dir : getAbstractDirs().entrySet()) { for (String path : dir.getValue().getFiles(true)) { files.add(dir.getKey() + separator + path); } } return files; } @Override public boolean containsFile(String path) { SubPath subpath; try { subpath = getSubPath(path); } catch (PathNotExist e) { return false; } if (subpath.dir != null) { return subpath.dir.containsFile(subpath.path); } return getFiles().contains(subpath.path); } @Override public boolean containsDir(String path) { SubPath subpath; try { subpath = getSubPath(path); } catch (PathNotExist e) { return false; } if (subpath.dir != null) { return subpath.dir.containsDir(subpath.path); } return getAbstractDirs().containsKey(subpath.path); } @Override public Map getDirs() throws UnsupportedOperationException { return getDirs(false); } @Override public Map getDirs(boolean recursive) throws UnsupportedOperationException { return new LinkedHashMap(getAbstractDirs(recursive)); } @Override public InputStream getFileInput(String path) throws DirectoryException { SubPath subpath = getSubPath(path); if (subpath.dir != null) { return subpath.dir.getFileInput(subpath.path); } if (!getFiles().contains(subpath.path)) { throw new PathNotExist(path); } return getFileInputLocal(subpath.path); } @Override public OutputStream getFileOutput(String path) throws DirectoryException { ParsedPath parsed = parsePath(path); if (parsed.dir == null) { getFiles().add(parsed.subpath); return getFileOutputLocal(parsed.subpath); } Directory dir; // IMPOSSIBLE_EXCEPTION try { dir = createDir(parsed.dir); } catch (PathAlreadyExists e) { dir = getAbstractDirs().get(parsed.dir); } return dir.getFileOutput(parsed.subpath); } @Override public Directory getDir(String path) throws PathNotExist { SubPath subpath = getSubPath(path); if (subpath.dir != null) { return subpath.dir.getDir(subpath.path); } if (!getAbstractDirs().containsKey(subpath.path)) { throw new PathNotExist(path); } return getAbstractDirs().get(subpath.path); } @Override public Directory createDir(String path) throws DirectoryException { ParsedPath parsed = parsePath(path); AbstractDirectory dir; if (parsed.dir == null) { if (getAbstractDirs().containsKey(parsed.subpath)) { throw new PathAlreadyExists(path); } dir = createDirLocal(parsed.subpath); getAbstractDirs().put(parsed.subpath, dir); return dir; } if (getAbstractDirs().containsKey(parsed.dir)) { dir = getAbstractDirs().get(parsed.dir); } else { dir = createDirLocal(parsed.dir); getAbstractDirs().put(parsed.dir, dir); } return dir.createDir(parsed.subpath); } @Override public boolean removeFile(String path) { SubPath subpath; try { subpath = getSubPath(path); } catch (PathNotExist e) { return false; } if (subpath.dir != null) { return subpath.dir.removeFile(subpath.path); } if (!getFiles().contains(subpath.path)) { return false; } removeFileLocal(subpath.path); getFiles().remove(subpath.path); return true; } protected Map getAbstractDirs() { return getAbstractDirs(false); } protected Map getAbstractDirs(boolean recursive) { if (mDirs == null) { loadDirs(); } if (!recursive) { return mDirs; } Map dirs = new LinkedHashMap(mDirs); for (Map.Entry dir : getAbstractDirs().entrySet()) { for (Map.Entry subdir : dir.getValue().getAbstractDirs(true).entrySet()) { dirs.put(dir.getKey() + separator + subdir.getKey(), subdir.getValue()); } } return dirs; } private SubPath getSubPath(String path) throws PathNotExist { ParsedPath parsed = parsePath(path); if (parsed.dir == null) { return new SubPath(null, parsed.subpath); } if (!getAbstractDirs().containsKey(parsed.dir)) { throw new PathNotExist(path); } return new SubPath(getAbstractDirs().get(parsed.dir), parsed.subpath); } private ParsedPath parsePath(String path) { int pos = path.indexOf(separator); if (pos == -1) { return new ParsedPath(null, path); } return new ParsedPath(path.substring(0, pos), path.substring(pos + 1)); } abstract protected void loadFiles(); abstract protected void loadDirs(); abstract protected InputStream getFileInputLocal(String name) throws DirectoryException; abstract protected OutputStream getFileOutputLocal(String name) throws DirectoryException; abstract protected AbstractDirectory createDirLocal(String name) throws DirectoryException; abstract protected void removeFileLocal(String name); private class ParsedPath { public String dir; public String subpath; public ParsedPath(String dir, String subpath) { this.dir = dir; this.subpath = subpath; } } private class SubPath { public AbstractDirectory dir; public String path; public SubPath(AbstractDirectory dir, String path) { this.dir = dir; this.path = path; } } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/directory/Directory.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.directory; import java.io.InputStream; import java.io.OutputStream; import java.util.Map; import java.util.Set; public interface Directory { public final char separator = '/'; public Set getFiles(); public Set getFiles(boolean recursive); public Map getDirs(); public Map getDirs(boolean recursive); public boolean containsFile(String path); public boolean containsDir(String path); public InputStream getFileInput(String path) throws DirectoryException; public OutputStream getFileOutput(String path) throws DirectoryException; public Directory getDir(String path) throws PathNotExist; public Directory createDir(String path) throws DirectoryException; public boolean removeFile(String path); } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/directory/DirectoryException.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.directory; public class DirectoryException extends Exception { private static final long serialVersionUID = -8871963042836625387L; public DirectoryException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } public DirectoryException(String detailMessage) { super(detailMessage); } public DirectoryException(Throwable throwable) { super(throwable); } public DirectoryException() { super(); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/directory/FileDirectory.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.directory; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.util.LinkedHashMap; import java.util.LinkedHashSet; public class FileDirectory extends AbstractDirectory { private File mDir; public FileDirectory(String dir) throws DirectoryException { this(new File(dir)); } public FileDirectory(File dir) throws DirectoryException { super(); if (!dir.isDirectory()) { throw new DirectoryException("file must be a directory: " + dir); } mDir = dir; } @Override protected AbstractDirectory createDirLocal(String name) throws DirectoryException { File dir = new File(generatePath(name)); dir.mkdir(); return new FileDirectory(dir); } @Override protected InputStream getFileInputLocal(String name) throws DirectoryException { try { return new FileInputStream(generatePath(name)); } catch (FileNotFoundException e) { throw new DirectoryException(e); } } @Override protected OutputStream getFileOutputLocal(String name) throws DirectoryException { try { return new FileOutputStream(generatePath(name)); } catch (FileNotFoundException e) { throw new DirectoryException(e); } } @Override protected void loadDirs() { loadAll(); } @Override protected void loadFiles() { loadAll(); } @Override protected void removeFileLocal(String name) { new File(generatePath(name)).delete(); } private String generatePath(String name) { return getDir().getPath() + separator + name; } private void loadAll() { mFiles = new LinkedHashSet(); mDirs = new LinkedHashMap(); File[] files = getDir().listFiles(); for (int i = 0; i < files.length; i++) { File file = files[i]; if (file.isFile()) { mFiles.add(file.getName()); } else { // IMPOSSIBLE_EXCEPTION try { mDirs.put(file.getName(), new FileDirectory(file)); } catch (DirectoryException e) { } } } } private File getDir() { return mDir; } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/directory/PathAlreadyExists.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.directory; public class PathAlreadyExists extends DirectoryException { private static final long serialVersionUID = 3776428251424428904L; public PathAlreadyExists() { } public PathAlreadyExists(Throwable throwable) { super(throwable); } public PathAlreadyExists(String detailMessage) { super(detailMessage); } public PathAlreadyExists(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/directory/PathNotExist.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.directory; public class PathNotExist extends DirectoryException { private static final long serialVersionUID = -6949242015506342032L; public PathNotExist() { super(); } public PathNotExist(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } public PathNotExist(String detailMessage) { super(detailMessage); } public PathNotExist(Throwable throwable) { super(throwable); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/directory/ZipRODirectory.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.directory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class ZipRODirectory extends AbstractDirectory { private ZipFile mZipFile; private String mPath; public ZipRODirectory(String zipFileName) throws DirectoryException { this(zipFileName, ""); } public ZipRODirectory(File zipFile) throws DirectoryException { this(zipFile, ""); } public ZipRODirectory(ZipFile zipFile) { this(zipFile, ""); } public ZipRODirectory(String zipFileName, String path) throws DirectoryException { this(new File(zipFileName), path); } public ZipRODirectory(File zipFile, String path) throws DirectoryException { super(); try { mZipFile = new ZipFile(zipFile); } catch (IOException e) { throw new DirectoryException(e); } mPath = path; } public ZipRODirectory(ZipFile zipFile, String path) { super(); mZipFile = zipFile; mPath = path; } @Override protected AbstractDirectory createDirLocal(String name) throws DirectoryException { throw new UnsupportedOperationException(); } @Override protected InputStream getFileInputLocal(String name) throws DirectoryException { try { return getZipFile().getInputStream(new ZipEntry(getPath() + name)); } catch (IOException e) { throw new PathNotExist(name, e); } } @Override protected OutputStream getFileOutputLocal(String name) throws DirectoryException { throw new UnsupportedOperationException(); } @Override protected void loadDirs() { loadAll(); } @Override protected void loadFiles() { loadAll(); } @Override protected void removeFileLocal(String name) { throw new UnsupportedOperationException(); } private void loadAll() { mFiles = new LinkedHashSet<>(); mDirs = new LinkedHashMap<>(); int prefixLen = getPath().length(); Enumeration entries = getZipFile().entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String name = entry.getName(); if (name.equals(getPath()) || !name.startsWith(getPath())) { continue; } String subname = name.substring(prefixLen); int pos = subname.indexOf(separator); if (pos == -1) { if (!entry.isDirectory()) { mFiles.add(subname); continue; } } else { subname = subname.substring(0, pos); } if (!mDirs.containsKey(subname)) { AbstractDirectory dir = new ZipRODirectory(getZipFile(), getPath() + subname + separator); mDirs.put(subname, dir); } } } private String getPath() { return mPath; } private ZipFile getZipFile() { return mZipFile; } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/resourceproguard/Configuration.java ================================================ package com.tencent.mm.resourceproguard; import com.tencent.mm.util.Utils; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * @author shwenzhang */ public class Configuration { public static final String DEFAULT_DIGEST_ALG = "SHA1"; public static final String ASRC_FILE = "resources.arsc"; private static final String TAG_ISSUE = "issue"; private static final String ATTR_VALUE = "value"; private static final String ATTR_ID = "id"; private static final String ATTR_ACTIVE = "isactive"; private static final String PROPERTY_ISSUE = "property"; private static final String WHITELIST_ISSUE = "whitelist"; private static final String COMPRESS_ISSUE = "compress"; private static final String MAPPING_ISSUE = "keepmapping"; private static final String SIGN_ISSUE = "sign"; private static final String ATTR_7ZIP = "seventzip"; private static final String ATTR_KEEPROOT = "keeproot"; private static final String ATTR_SIGNFILE = "metaname"; private static final String MERGE_DUPLICATED_RES = "mergeDuplicatedRes"; private static final String ATTR_SIGNFILE_PATH = "path"; private static final String ATTR_SIGNFILE_KEYPASS = "keypass"; private static final String ATTR_SIGNFILE_STOREPASS = "storepass"; private static final String ATTR_SIGNFILE_ALIAS = "alias"; public final HashMap>> mWhiteList; public final HashMap>> mOldResMapping; public final HashMap mOldFileMapping; public final HashSet mCompressPatterns; public final String digestAlg; private final Pattern MAP_PATTERN = Pattern.compile("\\s+(.*)->(.*)"); public boolean mUse7zip = true; public boolean mKeepRoot = false; public boolean mMergeDuplicatedRes = false; public String mMetaName = "META-INF"; public String mFixedResName = null; public boolean mUseSignAPK = false; public boolean mUseKeepMapping = false; public File mSignatureFile; public File mOldMappingFile; public boolean mUseWhiteList; public boolean mUseCompress; public String mKeyPass; public String mStorePass; public String mStoreAlias; public String m7zipPath; public String mZipalignPath; /** * use by command line with xml config * * @param config xml config file * @param sevenzipPath 7zip bin file path * @param zipAlignPath zipalign bin file path * @param mappingFile mapping file * @param signatureFile signature file * @param keypass signature key password * @param storealias signature store alias * @param storepass signature store password * @throws IOException io exception * @throws ParserConfigurationException parse exception * @throws SAXException sax exception */ public Configuration( File config, String sevenzipPath, String zipAlignPath, File mappingFile, File signatureFile, String keypass, String storealias, String storepass) throws IOException, ParserConfigurationException, SAXException { mWhiteList = new HashMap<>(); mOldResMapping = new HashMap<>(); mOldFileMapping = new HashMap<>(); mCompressPatterns = new HashSet<>(); digestAlg = DEFAULT_DIGEST_ALG; if (signatureFile != null) { setSignData(signatureFile, keypass, storealias, storepass); } if (mappingFile != null) { setKeepMappingData(mappingFile); } // setSignData and setKeepMappingData must before readXmlConfig or it will read readXmlConfig(config); this.m7zipPath = sevenzipPath; this.mZipalignPath = zipAlignPath; } /** * use by gradle * * @param param {@link InputParam} parameter * @throws IOException io exception */ public Configuration(InputParam param) throws IOException { mWhiteList = new HashMap<>(); mOldResMapping = new HashMap<>(); mOldFileMapping = new HashMap<>(); mCompressPatterns = new HashSet<>(); this.digestAlg = param.digestAlg; if (param.useSign) { setSignData(param.signFile, param.keypass, param.storealias, param.storepass); } if (param.mappingFile != null) { mUseKeepMapping = true; setKeepMappingData(param.mappingFile); } for (String item : param.whiteList) { mUseWhiteList = true; addWhiteList(item); } mUse7zip = param.use7zip; mKeepRoot = param.keepRoot; mMergeDuplicatedRes = param.mergeDuplicatedRes; mMetaName = param.metaName; mFixedResName = param.fixedResName; for (String item : param.compressFilePattern) { mUseCompress = true; addToCompressPatterns(item); } this.m7zipPath = param.sevenZipPath; this.mZipalignPath = param.zipAlignPath; } private void setSignData( File signatureFile, String keypass, String storealias, String storepass) throws IOException { mUseSignAPK = true; mSignatureFile = signatureFile; if (!mSignatureFile.exists()) { throw new IOException(String.format("the signature file do not exit, raw path= %s\n", mSignatureFile.getAbsolutePath() )); } mKeyPass = keypass; mStoreAlias = storealias; mStorePass = storepass; } private void setKeepMappingData(File mappingFile) throws IOException { if (mUseKeepMapping) { mOldMappingFile = mappingFile; if (!mOldMappingFile.exists()) { throw new IOException(String.format("the old mapping file do not exit, raw path= %s", mOldMappingFile.getAbsolutePath() )); } processOldMappingFile(); } } private void readXmlConfig(File xmlConfigFile) throws IOException, ParserConfigurationException, SAXException { if (!xmlConfigFile.exists()) { return; } System.out.printf("reading config file, %s\n", xmlConfigFile.getAbsolutePath()); BufferedInputStream input = null; try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); input = new BufferedInputStream(new FileInputStream(xmlConfigFile)); InputSource source = new InputSource(input); factory.setNamespaceAware(false); factory.setValidating(false); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(source); NodeList issues = document.getElementsByTagName(TAG_ISSUE); for (int i = 0, count = issues.getLength(); i < count; i++) { Node node = issues.item(i); Element element = (Element) node; String id = element.getAttribute(ATTR_ID); String isActive = element.getAttribute(ATTR_ACTIVE); if (id.length() == 0) { System.err.println("Invalid config file: Missing required issue id attribute"); continue; } boolean active = isActive != null && isActive.equals("true"); switch (id) { case PROPERTY_ISSUE: readPropertyFromXml(node); break; case WHITELIST_ISSUE: mUseWhiteList = active; if (mUseWhiteList) { readWhiteListFromXml(node); } break; case COMPRESS_ISSUE: mUseCompress = active; if (mUseCompress) { readCompressFromXml(node); } break; case SIGN_ISSUE: mUseSignAPK |= active; if (mUseSignAPK) { readSignFromXml(node, xmlConfigFile.getParentFile()); } break; case MAPPING_ISSUE: mUseKeepMapping = active; if (mUseKeepMapping) { loadMappingFilesFromXml(node); } break; default: System.err.println("unknown issue " + id); break; } } } finally { if (input != null) { try { input.close(); } catch (IOException e) { System.exit(-1); } } } } private void readWhiteListFromXml(Node node) throws IOException { NodeList childNodes = node.getChildNodes(); if (childNodes.getLength() > 0) { for (int j = 0, n = childNodes.getLength(); j < n; j++) { Node child = childNodes.item(j); if (child.getNodeType() == Node.ELEMENT_NODE) { Element check = (Element) child; String vaule = check.getAttribute(ATTR_VALUE); addWhiteList(vaule); } } } } private void addWhiteList(String item) throws IOException { if (item.length() == 0) { throw new IOException("Invalid config file: Missing required attribute " + ATTR_VALUE); } int packagePos = item.indexOf(".R."); if (packagePos == -1) { throw new IOException(String.format("please write the full package name,eg com.tencent.mm.R.drawable.dfdf, but yours %s\n", item )); } //先去掉空格 item = item.trim(); String packageName = item.substring(0, packagePos); //不能通过lastDot int nextDot = item.indexOf(".", packagePos + 3); String typeName = item.substring(packagePos + 3, nextDot); String name = item.substring(nextDot + 1); HashMap> typeMap; if (mWhiteList.containsKey(packageName)) { typeMap = mWhiteList.get(packageName); } else { typeMap = new HashMap<>(); } HashSet patterns; if (typeMap.containsKey(typeName)) { patterns = typeMap.get(typeName); } else { patterns = new HashSet<>(); } name = Utils.convertToPatternString(name); Pattern pattern = Pattern.compile(name); patterns.add(pattern); typeMap.put(typeName, patterns); System.out.println(String.format("convertToPatternString typeName %s format %s", typeName, name)); mWhiteList.put(packageName, typeMap); } private void readSignFromXml(Node node, File xmlConfigFileParentFile) throws IOException { if (mSignatureFile != null) { System.err.println("already set the sign info from command line, ignore this"); return; } NodeList childNodes = node.getChildNodes(); if (childNodes.getLength() > 0) { for (int j = 0, n = childNodes.getLength(); j < n; j++) { Node child = childNodes.item(j); if (child.getNodeType() == Node.ELEMENT_NODE) { Element check = (Element) child; String tagName = check.getTagName(); String vaule = check.getAttribute(ATTR_VALUE); if (vaule.length() == 0) { throw new IOException(String.format("Invalid config file: Missing required attribute %s\n", ATTR_VALUE)); } switch (tagName) { case ATTR_SIGNFILE_PATH: char ch = vaule.charAt(0); switch (ch) { // supports the writting style like ~/.android/debug.keystore. the symbol ~ represent the home directory of the current user. case '~': mSignatureFile = new File(String.format("%s%s", System.getProperty("user.home"), vaule.substring(1))); break; // relative to the directory of the xml config file. case '.': mSignatureFile = new File(xmlConfigFileParentFile, vaule); break; // keep the origin logical. default: mSignatureFile = new File(vaule); } if (!mSignatureFile.isFile()) { throw new IOException(String.format("the signature file do not exit. raw path= %s\n", mSignatureFile.getAbsolutePath() )); } break; case ATTR_SIGNFILE_STOREPASS: mStorePass = vaule; mStorePass = mStorePass.trim(); break; case ATTR_SIGNFILE_KEYPASS: mKeyPass = vaule; mKeyPass = mKeyPass.trim(); break; case ATTR_SIGNFILE_ALIAS: mStoreAlias = vaule; mStoreAlias = mStoreAlias.trim(); break; default: System.err.println("unknown tag " + tagName); break; } } } } } private void readCompressFromXml(Node node) throws IOException { NodeList childNodes = node.getChildNodes(); if (childNodes.getLength() > 0) { for (int j = 0, n = childNodes.getLength(); j < n; j++) { Node child = childNodes.item(j); if (child.getNodeType() == Node.ELEMENT_NODE) { Element check = (Element) child; String value = check.getAttribute(ATTR_VALUE); addToCompressPatterns(value); } } } } private void addToCompressPatterns(String value) throws IOException { if (value.length() == 0) { throw new IOException(String.format("Invalid config file: Missing required attribute %s\n", ATTR_VALUE)); } value = Utils.convertToPatternString(value); Pattern pattern = Pattern.compile(value); mCompressPatterns.add(pattern); } private void loadMappingFilesFromXml(Node node) throws IOException { if (mOldMappingFile != null) { System.err.println("Mapping file already load from command line, ignore this config"); return; } NodeList childNodes = node.getChildNodes(); if (childNodes.getLength() > 0) { for (int j = 0, n = childNodes.getLength(); j < n; j++) { Node child = childNodes.item(j); if (child.getNodeType() == Node.ELEMENT_NODE) { Element check = (Element) child; String filePath = check.getAttribute(ATTR_VALUE); if (filePath.length() == 0) { throw new IOException(String.format("Invalid config file: Missing required attribute %s\n", ATTR_VALUE)); } readOldMapping(filePath); } } } } private void readPropertyFromXml(Node node) throws IOException { NodeList childNodes = node.getChildNodes(); if (childNodes.getLength() > 0) { for (int j = 0, n = childNodes.getLength(); j < n; j++) { Node child = childNodes.item(j); if (child.getNodeType() == Node.ELEMENT_NODE) { Element check = (Element) child; String tagName = check.getTagName(); String vaule = check.getAttribute(ATTR_VALUE); if (vaule.length() == 0) { throw new IOException(String.format("Invalid config file: Missing required attribute %s\n", ATTR_VALUE)); } switch (tagName) { case ATTR_7ZIP: mUse7zip = vaule.equals("true"); break; case ATTR_KEEPROOT: mKeepRoot = vaule.equals("true"); System.out.println("mKeepRoot " + mKeepRoot); break; case MERGE_DUPLICATED_RES: mMergeDuplicatedRes = vaule.equals("true"); System.out.println("mMergeDuplicatedRes " + mMergeDuplicatedRes); break; case ATTR_SIGNFILE: mMetaName = vaule.trim(); break; default: System.err.println("unknown tag " + tagName); break; } } } } } private void readOldMapping(String filePath) throws IOException { mOldMappingFile = new File(filePath); if (!mOldMappingFile.exists()) { throw new IOException(String.format("the old mapping file do not exit, raw path= %s\n", mOldMappingFile.getAbsolutePath() )); } processOldMappingFile(); System.out.printf("you are using the keepmapping mode to proguard resouces: old mapping path:%s\n", mOldMappingFile.getAbsolutePath() ); } private void processOldMappingFile() throws IOException { mOldResMapping.clear(); mOldFileMapping.clear(); FileReader fr; try { fr = new FileReader(mOldMappingFile); } catch (FileNotFoundException ex) { throw new IOException(String.format("Could not find old mapping file %s", mOldMappingFile.getAbsolutePath())); } BufferedReader br = new BufferedReader(fr); try { String line = br.readLine(); while (line != null) { if (line.length() > 0) { Matcher mat = MAP_PATTERN.matcher(line); if (mat.find()) { String nameAfter = mat.group(2); String nameBefore = mat.group(1); nameAfter = nameAfter.trim(); nameBefore = nameBefore.trim(); //如果有这个的话,那就是mOldFileMapping if (line.contains("/")) { mOldFileMapping.put(nameBefore, nameAfter); } else { //这里是resid的mapping int packagePos = nameBefore.indexOf(".R."); if (packagePos == -1) { throw new IOException(String.format("the old mapping file packagename is malformed, " + "it should be like com.tencent.mm.R.attr.test, yours %s\n", nameBefore )); } String packageName = nameBefore.substring(0, packagePos); int nextDot = nameBefore.indexOf(".", packagePos + 3); String typeName = nameBefore.substring(packagePos + 3, nextDot); String beforename = nameBefore.substring(nextDot + 1); String aftername = nameAfter.substring(nameAfter.indexOf(".", packagePos + 3) + 1); HashMap> typeMap; if (mOldResMapping.containsKey(packageName)) { typeMap = mOldResMapping.get(packageName); } else { typeMap = new HashMap<>(); } HashMap namesMap; if (typeMap.containsKey(typeName)) { namesMap = typeMap.get(typeName); } else { namesMap = new HashMap<>(); } namesMap.put(beforename, aftername); typeMap.put(typeName, namesMap); mOldResMapping.put(packageName, typeMap); } } } line = br.readLine(); } } catch (IOException ex) { throw new RuntimeException("Error while mapping file"); } finally { try { br.close(); fr.close(); } catch (IOException e) { e.printStackTrace(); } } } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/resourceproguard/InputParam.java ================================================ package com.tencent.mm.resourceproguard; import com.tencent.mm.androlib.res.util.StringUtil; import java.io.File; import java.util.ArrayList; public class InputParam { public final File mappingFile; public final boolean use7zip; public final boolean keepRoot; public final boolean mergeDuplicatedRes; public final boolean useSign; public final String metaName; public final String fixedResName; public final ArrayList whiteList; public final ArrayList compressFilePattern; public final String apkPath; public final String outFolder; public final File signFile; public final String keypass; public final String storealias; public final String storepass; public final String zipAlignPath; public final String sevenZipPath; public final SignatureType signatureType; public final String finalApkBackupPath; public final String digestAlg; public final int minSDKVersion; public final int targetSDKVersion; private InputParam( File mappingFile, boolean use7zip, boolean useSign, boolean keepRoot, boolean mergeDuplicatedRes, ArrayList whiteList, ArrayList compressFilePattern, String apkPath, String outFolder, File signFile, String keypass, String storealias, String storepass, String metaName, String fixedResName, String zipAlignPath, String sevenZipPath, SignatureType signatureType, String finalApkBackupPath, String digestAlg, int minSDKVersion, int targetSDKVersion) { this.mappingFile = mappingFile; this.use7zip = use7zip; this.useSign = useSign; this.keepRoot = keepRoot; this.mergeDuplicatedRes = mergeDuplicatedRes; this.whiteList = whiteList; this.compressFilePattern = compressFilePattern; this.apkPath = apkPath; this.outFolder = outFolder; this.signFile = signFile; this.keypass = keypass; this.storealias = storealias; this.storepass = storepass; this.metaName = metaName; this.fixedResName = fixedResName; this.zipAlignPath = zipAlignPath; this.sevenZipPath = sevenZipPath; this.signatureType = signatureType; this.finalApkBackupPath = finalApkBackupPath; this.digestAlg = digestAlg; this.minSDKVersion = minSDKVersion; this.targetSDKVersion = targetSDKVersion; } public enum SignatureType { SchemaV1, SchemaV2, SchemaV3 } public static class Builder { private File mappingFile; private boolean use7zip; private boolean useSign; private boolean keepRoot; private boolean mergeDuplicatedRes; private ArrayList whiteList; private ArrayList compressFilePattern; private String apkPath; private String outFolder; private File signFile; private String keypass; private String storealias; private String storepass; private String metaName; private String fixedResName; private String zipAlignPath; private String sevenZipPath; private SignatureType signatureType; private String finalApkBackupPath; private String digestAlg; private int minSDKVersion; private int targetSDKVersion; public Builder() { use7zip = false; keepRoot = false; signatureType = SignatureType.SchemaV1; } public Builder setMappingFile(File mappingFile) { this.mappingFile = mappingFile; return this; } public Builder setUse7zip(boolean use7zip) { this.use7zip = use7zip; return this; } public Builder setUseSign(boolean useSign) { this.useSign = useSign; return this; } public Builder setKeepRoot(boolean keepRoot) { this.keepRoot = keepRoot; return this; } public Builder setMergeDuplicatedRes(boolean mergeDuplicatedRes) { this.mergeDuplicatedRes = mergeDuplicatedRes; return this; } public Builder setWhiteList(ArrayList whiteList) { this.whiteList = whiteList; return this; } public Builder setCompressFilePattern(ArrayList compressFilePattern) { if (compressFilePattern.contains(Configuration.ASRC_FILE)) { System.out.printf("[Warning] compress %s will prevent optimization at runtime", Configuration.ASRC_FILE); } this.compressFilePattern = compressFilePattern; return this; } public Builder setApkPath(String apkPath) { this.apkPath = apkPath; return this; } public Builder setOutBuilder(String outFolder) { this.outFolder = outFolder; return this; } public Builder setSignFile(File signFile) { this.signFile = signFile; return this; } public Builder setKeypass(String keypass) { this.keypass = keypass; return this; } public Builder setStorealias(String storealias) { this.storealias = storealias; return this; } public Builder setStorepass(String storepass) { this.storepass = storepass; return this; } public Builder setMetaName(String metaName) { this.metaName = metaName; return this; } public Builder setFixedResName(String fixedResName) { this.fixedResName = fixedResName; return this; } public Builder setZipAlign(String zipAlignPath) { this.zipAlignPath = zipAlignPath; return this; } public Builder setSevenZipPath(String sevenZipPath) { this.sevenZipPath = sevenZipPath; return this; } public Builder setSignatureType(SignatureType signatureType) { this.signatureType = signatureType; return this; } public Builder setFinalApkBackupPath(String finalApkBackupPath) { this.finalApkBackupPath = finalApkBackupPath; return this; } public Builder setDigestAlg(String digestAlg) { if (StringUtil.isPresent(digestAlg)) { this.digestAlg = digestAlg; } else { this.digestAlg = Configuration.DEFAULT_DIGEST_ALG; } return this; } public Builder setMinSDKVersion(int minSDKVersion) { this.minSDKVersion = minSDKVersion; return this; } public Builder setTargetSDKVersion(int targetSDKVersion) { this.targetSDKVersion = targetSDKVersion; return this; } public InputParam create() { if (targetSDKVersion >= 30) { // Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs // to be stored uncompressed and aligned on a 4-byte boundary this.compressFilePattern.remove(Configuration.ASRC_FILE); System.out.printf("[Warning] Remove resources.arsc from the compressPattern. (%s)\n", this.compressFilePattern); } return new InputParam(mappingFile, use7zip, useSign, keepRoot, mergeDuplicatedRes, whiteList, compressFilePattern, apkPath, outFolder, signFile, keypass, storealias, storepass, metaName, fixedResName, zipAlignPath, sevenZipPath, signatureType, finalApkBackupPath, digestAlg, minSDKVersion, targetSDKVersion ); } } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/resourceproguard/Main.java ================================================ package com.tencent.mm.resourceproguard; import com.tencent.mm.androlib.AndrolibException; import com.tencent.mm.androlib.ApkDecoder; import com.tencent.mm.androlib.ResourceApkBuilder; import com.tencent.mm.androlib.res.decoder.ARSCDecoder; import com.tencent.mm.androlib.res.util.StringUtil; import com.tencent.mm.directory.DirectoryException; import com.tencent.mm.util.FileOperation; import java.io.File; import java.io.IOException; /** * @author shwenzhang * @author simsun */ public class Main { public static final int ERRNO_ERRORS = 1; public static final int ERRNO_USAGE = 2; protected static long mRawApkSize; protected static String mRunningLocation; protected static long mBeginTime; /** * 是否通过命令行方式设置 **/ public boolean mSetSignThroughCmd; public boolean mSetMappingThroughCmd; public String m7zipPath; public String mZipalignPath; public String mFinalApkBackPath; protected Configuration config; protected File mOutDir; public static void gradleRun(InputParam inputParam) { Main m = new Main(); m.run(inputParam); } private void run(InputParam inputParam) { synchronized (Main.class) { loadConfigFromGradle(inputParam); this.mFinalApkBackPath = inputParam.finalApkBackupPath; Thread currentThread = Thread.currentThread(); System.out.printf( "\n-->AndResGuard starting! Current thread# id: %d, name: %s\n", currentThread.getId(), currentThread.getName() ); File finalApkFile = StringUtil.isPresent(inputParam.finalApkBackupPath) ? new File(inputParam.finalApkBackupPath) : null; resourceProguard( new File(inputParam.outFolder), finalApkFile, inputParam.apkPath, inputParam.signatureType, inputParam.minSDKVersion ); System.out.printf("<--AndResGuard Done! You can find the output in %s\n", mOutDir.getAbsolutePath()); clean(); } } protected void clean() { config = null; ARSCDecoder.mTableStringsResguard.clear(); ARSCDecoder.mMergeDuplicatedResCount = 0; } private void loadConfigFromGradle(InputParam inputParam) { try { config = new Configuration(inputParam); } catch (IOException e) { e.printStackTrace(); } } protected void resourceProguard( File outputDir, File outputFile, String apkFilePath, InputParam.SignatureType signatureType) { resourceProguard(outputDir, outputFile, apkFilePath, signatureType, 14 /*default min sdk*/); } protected void resourceProguard( File outputDir, File outputFile, String apkFilePath, InputParam.SignatureType signatureType, int minSDKVersoin) { File apkFile = new File(apkFilePath); if (!apkFile.exists()) { System.err.printf("The input apk %s does not exist", apkFile.getAbsolutePath()); goToError(); } mRawApkSize = FileOperation.getFileSizes(apkFile); try { ApkDecoder decoder = new ApkDecoder(config, apkFile); /* 默认使用V1签名 */ decodeResource(outputDir, decoder, apkFile); buildApk(decoder, apkFile, outputFile, signatureType, minSDKVersoin); } catch (Exception e) { e.printStackTrace(); goToError(); } } private void decodeResource(File outputFile, ApkDecoder decoder, File apkFile) throws AndrolibException, IOException, DirectoryException { if (outputFile == null) { mOutDir = new File(mRunningLocation, apkFile.getName().substring(0, apkFile.getName().indexOf(".apk"))); } else { mOutDir = outputFile; } decoder.setOutDir(mOutDir.getAbsoluteFile()); decoder.decode(); } private void buildApk( ApkDecoder decoder, File apkFile, File outputFile, InputParam.SignatureType signatureType, int minSDKVersion) throws Exception { ResourceApkBuilder builder = new ResourceApkBuilder(config); String apkBasename = apkFile.getName(); apkBasename = apkBasename.substring(0, apkBasename.indexOf(".apk")); builder.setOutDir(mOutDir, apkBasename, outputFile); System.out.printf("[AndResGuard] buildApk signatureType: %s\n", signatureType); switch (signatureType) { case SchemaV1: builder.buildApkWithV1sign(decoder.getCompressData()); break; case SchemaV2: case SchemaV3: builder.buildApkWithV2V3Sign(decoder.getCompressData(), minSDKVersion, signatureType); break; } } protected void goToError() { System.exit(ERRNO_USAGE); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/util/DataInputDelegate.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.util; import java.io.DataInput; import java.io.IOException; /** * @author shwenzhang */ abstract public class DataInputDelegate implements DataInput { protected final DataInput mDelegate; public DataInputDelegate(DataInput delegate) { this.mDelegate = delegate; } public int skipBytes(int n) throws IOException { return mDelegate.skipBytes(n); } public int readUnsignedShort() throws IOException { return mDelegate.readUnsignedShort(); } public int readUnsignedByte() throws IOException { return mDelegate.readUnsignedByte(); } public String readUTF() throws IOException { return mDelegate.readUTF(); } public short readShort() throws IOException { return mDelegate.readShort(); } public long readLong() throws IOException { return mDelegate.readLong(); } public String readLine() throws IOException { return mDelegate.readLine(); } public int readInt() throws IOException { return mDelegate.readInt(); } public void readFully(byte[] b, int off, int len) throws IOException { mDelegate.readFully(b, off, len); } public void readFully(byte[] b) throws IOException { mDelegate.readFully(b); } public float readFloat() throws IOException { return mDelegate.readFloat(); } public double readDouble() throws IOException { return mDelegate.readDouble(); } public char readChar() throws IOException { return mDelegate.readChar(); } public byte readByte() throws IOException { return mDelegate.readByte(); } public boolean readBoolean() throws IOException { return mDelegate.readBoolean(); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/util/DataOutputDelegate.java ================================================ package com.tencent.mm.util; import java.io.DataOutput; import java.io.IOException; public class DataOutputDelegate implements DataOutput { protected final DataOutput mDelegate; public DataOutputDelegate(DataOutput delegate) { this.mDelegate = delegate; } @Override public void write(int b) throws IOException { // TODO Auto-generated method stub this.mDelegate.write(b); } @Override public void write(byte[] b) throws IOException { // TODO Auto-generated method stub this.mDelegate.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { // TODO Auto-generated method stub this.mDelegate.write(b, off, len); } @Override public void writeBoolean(boolean v) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeBoolean(v); } @Override public void writeByte(int v) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeByte(v); } @Override public void writeShort(int v) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeShort(v); } @Override public void writeChar(int v) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeChar(v); } @Override public void writeInt(int v) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeInt(v); } @Override public void writeLong(long v) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeLong(v); } @Override public void writeFloat(float v) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeFloat(v); } @Override public void writeDouble(double v) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeDouble(v); } @Override public void writeBytes(String s) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeBytes(s); } @Override public void writeChars(String s) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeChars(s); } @Override public void writeUTF(String s) throws IOException { // TODO Auto-generated method stub this.mDelegate.writeUTF(s); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/util/ExtDataInput.java ================================================ /** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at *

* http://www.apache.org/licenses/LICENSE-2.0 *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.util; import java.io.DataInput; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; /** * @author shwenzhang */ public class ExtDataInput extends DataInputDelegate { public ExtDataInput(InputStream in) { this((DataInput) new DataInputStream(in)); } public ExtDataInput(DataInput delegate) { super(delegate); } public int[] readIntArray(int length) throws IOException { int[] array = new int[length]; for (int i = 0; i < length; i++) { array[i] = readInt(); } return array; } public void skipInt() throws IOException { skipBytes(4); } public void skipCheckInt(int expected) throws IOException { int got = readInt(); if (got != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); } } public void skipCheckChunkTypeInt(int expected, int possible) throws IOException { int got = readInt(); if (got == possible) { skipCheckChunkTypeInt(expected, -1); } else if (got != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); } } public void skipCheckShort(short expected) throws IOException { short got = readShort(); if (got != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); } } public void skipCheckByte(byte expected) throws IOException { byte got = readByte(); if (got != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, got)); } } public String readNullEndedString(int length, boolean fixed) throws IOException { StringBuilder string = new StringBuilder(16); while (length-- != 0) { short ch = readShort(); if (ch == 0) { break; } string.append((char) ch); } if (fixed) { skipBytes(length * 2); } return string.toString(); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/util/ExtDataOutput.java ================================================ package com.tencent.mm.util; import java.io.DataOutput; import java.io.IOException; public class ExtDataOutput extends DataOutputDelegate { public ExtDataOutput(DataOutput delegate) { super(delegate); // TODO Auto-generated constructor stub } public void writeIntArray(int[] array) throws IOException { int length = array.length; for (int i = 0; i < length; i++) { writeInt(array[i]); } } public void writeBytes(ExtDataInput in, int length) throws IOException { byte[] data = new byte[length]; in.readFully(data); write(data); } public void writeCheckInt(int value, int expected) throws IOException { writeInt(value); if (value != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, value)); } } public void writeCheckChunkTypeInt(ExtDataInput reader, int expected, int possible) throws IOException { int value = reader.readInt(); writeInt(value); if (value == possible) { writeCheckChunkTypeInt(reader, expected, -1); } else if (value != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, value)); } } public void writeCheckShort(short value, short expected) throws IOException { writeShort(value); if (value != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, value)); } } public void writeCheckByte(byte value, byte expected) throws IOException { writeByte(value); if (value != expected) { throw new IOException(String.format("Expected: 0x%08x, got: 0x%08x", expected, value)); } } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/util/FileOperation.java ================================================ package com.tencent.mm.util; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.zip.CRC32; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; public class FileOperation { private static final int BUFFER = 8192; public static boolean fileExists(String filePath) { if (filePath == null) { return false; } File file = new File(filePath); if (file.exists()) return true; return false; } public static boolean deleteFile(String filePath) { if (filePath == null) { return true; } File file = new File(filePath); if (file.exists()) { return file.delete(); } return true; } public static long getlist(File f) { if (f == null || (!f.exists())) { return 0; } if (!f.isDirectory()) { return 1; } long size; File flist[] = f.listFiles(); size = flist.length; for (int i = 0; i < flist.length; i++) { if (flist[i].isDirectory()) { size = size + getlist(flist[i]); size--; } } return size; } public static long getFileSizes(File f) { long size = 0; if (f.exists() && f.isFile()) { FileInputStream fis = null; try { fis = new FileInputStream(f); size = fis.available(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (fis != null) { fis.close(); } } catch (IOException e) { e.printStackTrace(); } } } return size; } public static boolean deleteDir(File file) { if (file == null || (!file.exists())) { return false; } if (file.isFile()) { file.delete(); } else if (file.isDirectory()) { File files[] = file.listFiles(); for (int i = 0; i < files.length; i++) { deleteDir(files[i]); } } file.delete(); return true; } public static void copyFileUsingStream(File source, File dest) throws IOException { FileInputStream is = null; FileOutputStream os = null; File parent = dest.getParentFile(); if (parent != null && (!parent.exists())) { parent.mkdirs(); } try { is = new FileInputStream(source); os = new FileOutputStream(dest, false); byte[] buffer = new byte[BUFFER]; int length; while ((length = is.read(buffer)) > 0) { os.write(buffer, 0, length); } } finally { if (is != null) { is.close(); } if (os != null) { os.close(); } } } public static boolean checkDirectory(String dir) { File dirObj = new File(dir); deleteDir(dirObj); if (!dirObj.exists()) { dirObj.mkdirs(); } return true; } public static File checkFile(String dir) { deleteFile(dir); File file = new File(dir); try { file.createNewFile(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return file; } @SuppressWarnings("rawtypes") public static HashMap unZipAPk(String fileName, String filePath) throws IOException { checkDirectory(filePath); ZipFile zipFile = new ZipFile(fileName); Enumeration emu = zipFile.entries(); HashMap compress = new HashMap<>(); try { while (emu.hasMoreElements()) { ZipEntry entry = (ZipEntry) emu.nextElement(); if (entry.isDirectory()) { new File(filePath, entry.getName()).mkdirs(); continue; } BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry)); File file = new File(filePath + File.separator + entry.getName()); File parent = file.getParentFile(); if (parent != null && (!parent.exists())) { parent.mkdirs(); } //要用linux的斜杠 String compatibaleresult = entry.getName(); if (compatibaleresult.contains("\\")) { compatibaleresult = compatibaleresult.replace("\\", "/"); } compress.put(compatibaleresult, entry.getMethod()); FileOutputStream fos = new FileOutputStream(file); BufferedOutputStream bos = new BufferedOutputStream(fos, BUFFER); byte[] buf = new byte[BUFFER]; int len; while ((len = bis.read(buf, 0, BUFFER)) != -1) { fos.write(buf, 0, len); } bos.flush(); bos.close(); bis.close(); } } finally { zipFile.close(); } return compress; } /** * zip list of file * * @param resFileList file(dir) list * @param baseFolder file(dir) base folder, we should calc relative path of resFile with base * @param zipFile output zip file * @param compressData compress data * @throws IOException io exception */ public static void zipFiles( Collection resFileList, File baseFolder, File zipFile, HashMap compressData) throws IOException { ZipOutputStream zipOut = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile), BUFFER)); for (File resFile : resFileList) { if (resFile.exists()) { if (resFile.getAbsolutePath().contains(baseFolder.getAbsolutePath())) { String relativePath = baseFolder.toURI().relativize(resFile.getParentFile().toURI()).getPath(); // remove slash at end of relativePath if (relativePath.length() > 1) { relativePath = relativePath.substring(0, relativePath.length() - 1); } else { relativePath = ""; } zipFile(resFile, zipOut, relativePath, compressData); } else { zipFile(resFile, zipOut, "", compressData); } } } zipOut.close(); } private static void zipFile( File resFile, ZipOutputStream zipout, String rootpath, HashMap compressData) throws IOException { rootpath = rootpath + (rootpath.trim().length() == 0 ? "" : File.separator) + resFile.getName(); if (resFile.isDirectory()) { File[] fileList = resFile.listFiles(); for (File file : fileList) { zipFile(file, zipout, rootpath, compressData); } } else { final byte[] fileContents = readContents(resFile); //这里需要强转成linux格式,果然坑!! if (rootpath.contains("\\")) { rootpath = rootpath.replace("\\", "/"); } if (!compressData.containsKey(rootpath)) { System.err.printf(String.format("do not have the compress data path =%s in resource.asrc\n", rootpath)); //throw new IOException(String.format("do not have the compress data path=%s", rootpath)); return; } int compressMethod = compressData.get(rootpath); ZipEntry entry = new ZipEntry(rootpath); if (compressMethod == ZipEntry.DEFLATED) { entry.setMethod(ZipEntry.DEFLATED); } else { entry.setMethod(ZipEntry.STORED); entry.setSize(fileContents.length); final CRC32 checksumCalculator = new CRC32(); checksumCalculator.update(fileContents); entry.setCrc(checksumCalculator.getValue()); } zipout.putNextEntry(entry); zipout.write(fileContents); zipout.flush(); zipout.closeEntry(); } } private static byte[] readContents(final File file) throws IOException { final ByteArrayOutputStream output = new ByteArrayOutputStream(); final int bufferSize = 4096; try { final FileInputStream in = new FileInputStream(file); final BufferedInputStream bIn = new BufferedInputStream(in); int length; byte[] buffer = new byte[bufferSize]; byte[] bufferCopy; while ((length = bIn.read(buffer, 0, bufferSize)) != -1) { bufferCopy = new byte[length]; System.arraycopy(buffer, 0, bufferCopy, 0, length); output.write(bufferCopy); } bIn.close(); } finally { output.close(); } return output.toByteArray(); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/util/Md5Util.java ================================================ package com.tencent.mm.util; import org.apache.commons.io.FileUtils; import java.io.File; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; /** * @author ysbing */ public class Md5Util { public static String getMD5Str(String str) { MessageDigest digest; try { digest = MessageDigest.getInstance("MD5"); digest.update(str.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { return ""; } return bytesToHexString(digest.digest()); } public static String getMD5Str(File file) { if (!file.isFile()) { return ""; } MessageDigest digest; try { digest = MessageDigest.getInstance("MD5"); digest.update(FileUtils.readFileToByteArray(file)); } catch (Exception e) { return ""; } return bytesToHexString(digest.digest()); } public static String bytesToHexString(byte[] src) { if (src.length <= 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(src.length); for (byte b : src) { int v = b & 0xFF; String hv = Integer.toHexString(v); if (hv.length() < 2) { stringBuilder.append(0); } stringBuilder.append(hv); } return stringBuilder.toString(); } } ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/util/TypedValue.java ================================================ /* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.tencent.mm.util; import java.util.zip.ZipEntry; /** * Container for a dynamically typed data value. Primarily used with */ public class TypedValue { public static final String UNZIP_FILE_PATH = "temp"; public static final String COMMAND_7ZIP = "7za"; public static final String COMMAND_ZIPALIGIN = "zipalign"; public static final String OUT_7ZIP_FILE_PATH = "out_7zip"; /** * 是7zip压缩使用,把制定为不压缩的拷到一起 */ public static final String STORED_FILE_PATH = "storedfiles"; public static final String RES_FILE_PATH = "r"; public static final String RES_MAPPING_FILE = "resource_mapping_"; public static final String MERGE_DUPLICATED_RES_MAPPING_FILE = "merge_duplicated_res_mapping_"; public static final int ZIP_STORED = ZipEntry.STORED; public static final int ZIP_DEFLATED = ZipEntry.DEFLATED; public static final int JDK_6 = 6; public static final String TXT_FILE = ".txt"; public static final String XML_FILE = ".xml"; public static final String CONFIG_FILE = "config.xml"; /** * The value contains no data. */ public static final int TYPE_NULL = 0x00; public static final int TYPE_STRING = 0x03; }; ================================================ FILE: AndResGuard-core/src/main/java/com/tencent/mm/util/Utils.java ================================================ package com.tencent.mm.util; import com.tencent.mm.androlib.res.util.StringUtil; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; public class Utils { public static boolean isPresent(String str) { return str != null && str.length() > 0; } public static boolean isBlank(String str) { return !isPresent(str); } public static boolean isPresent(Iterator iterator) { return iterator != null && iterator.hasNext(); } public static boolean isBlank(Iterator iterator) { return !isPresent(iterator); } public static String convertToPatternString(String input) { // ? Zero or one character // * Zero or more of character // + One or more of character final String[] searchList = new String[] { ".", "?", "*", "+" }; final String[] replacementList = new String[] { "\\.", ".?", ".*", ".+" }; return replaceEach(input, searchList, replacementList); } public static boolean match(String str, HashSet patterns) { if (patterns == null) { return false; } for(Pattern p : patterns) { Boolean isMatch = p.matcher(str).matches(); if (isMatch) return true; } return false; } public static void cleanDir(File dir) { if (dir.exists()) { FileOperation.deleteDir(dir); dir.mkdirs(); } } public static String runCmd(String... cmd) throws IOException, InterruptedException { String output; Process process = null; try { process = new ProcessBuilder(cmd).start(); output = StringUtil.readInputStream(process.getInputStream()); process.waitFor(); if (process.exitValue() != 0) { System.err.println(String.format("%s Failed! Please check your signature file.\n", cmd[0])); throw new RuntimeException(StringUtil.readInputStream(process.getErrorStream())); } } finally { if (process != null) { process.destroy(); } } return output; } public static String runExec(String[] argv) throws IOException, InterruptedException { Process process = null; String output; try { process = Runtime.getRuntime().exec(argv); output = StringUtil.readInputStream(process.getInputStream()); process.waitFor(); if (process.exitValue() != 0) { System.err.println(String.format("%s Failed! Please check your signature file.\n", argv[0])); throw new RuntimeException(StringUtil.readInputStream(process.getErrorStream())); } } finally { if (process != null) { process.destroy(); } } return output; } private static void processOutputStreamInThread(Process process) throws IOException { InputStreamReader ir = new InputStreamReader(process.getInputStream()); LineNumberReader input = new LineNumberReader(ir); //如果不读会有问题,被阻塞 while (input.readLine() != null) { } } private static String replaceEach(String text, String[] searchList, String[] replacementList) { // TODO: throw new IllegalArgumentException() if any param doesn't make sense //validateParams(text, searchList, replacementList); SearchTracker tracker = new SearchTracker(text, searchList, replacementList); if (!tracker.hasNextMatch(0)) { return text; } StringBuilder buf = new StringBuilder(text.length() * 2); int start = 0; do { SearchTracker.MatchInfo matchInfo = tracker.matchInfo; int textIndex = matchInfo.textIndex; String pattern = matchInfo.pattern; String replacement = matchInfo.replacement; buf.append(text.substring(start, textIndex)); buf.append(replacement); start = textIndex + pattern.length(); } while (tracker.hasNextMatch(start)); return buf.append(text.substring(start)).toString(); } static class SearchTracker { final String text; final Map patternToReplacement = new HashMap<>(); final Set pendingPatterns = new HashSet<>(); MatchInfo matchInfo = null; SearchTracker(String text, String[] searchList, String[] replacementList) { this.text = text; for (int i = 0; i < searchList.length; ++i) { String pattern = searchList[i]; patternToReplacement.put(pattern, replacementList[i]); pendingPatterns.add(pattern); } } boolean hasNextMatch(int start) { int textIndex = -1; String nextPattern = null; for (String pattern : new ArrayList<>(pendingPatterns)) { int matchIndex = text.indexOf(pattern, start); if (matchIndex == -1) { pendingPatterns.remove(pattern); } else { if (textIndex == -1 || matchIndex < textIndex) { textIndex = matchIndex; nextPattern = pattern; } } } if (nextPattern != null) { matchInfo = new MatchInfo(nextPattern, patternToReplacement.get(nextPattern), textIndex); return true; } return false; } private static class MatchInfo { final String pattern; final String replacement; final int textIndex; MatchInfo(String pattern, String replacement, int textIndex) { this.pattern = pattern; this.replacement = replacement; this.textIndex = textIndex; } } } } ================================================ FILE: AndResGuard-example/.gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures **/fabric.properties .gradletasknamecache ================================================ FILE: AndResGuard-example/app/.gitignore ================================================ /build ================================================ FILE: AndResGuard-example/app/build.gradle ================================================ apply plugin: 'AndResGuard' apply plugin: 'com.android.application' buildscript { repositories { mavenLocal() google() jcenter() } dependencies { // The Fabric Gradle plugin uses an open ended version to react // quickly to Android tooling updates classpath("com.tencent.mm:AndResGuard-gradle-plugin:${ANDRESGUARD_VERSION}") { changing = true } } } andResGuard { mappingFile = file("./resource_mapping.txt") use7zip = true useSign = true keepRoot = false mergeDuplicatedRes = true // add .R.drawable.icon into whitelist. // because the launcher will get the icon with his name whiteList = ["R.mipmap.ic_launcher", //https://docs.fabric.io/android/crashlytics/build-tools.html "R.string.com.crashlytics.*", "R.id.*"] compressFilePattern = ["*.png", "*.jpg", "*.jpeg", "*.gif", "resources.arsc"] sevenzip { artifact = "com.tencent.mm:SevenZip:${ANDRESGUARD_SEVENZIP_VERSION}" //path = "/usr/local/bin/7za" } /** * Optional: if finalApkBackupPath is null, AndResGuard will overwrite final apk * to the path which assemble[Task] write to*/ finalApkBackupPath = "${project.rootDir}/final.apk" digestalg = "SHA-256" } repositories { jcenter() maven { url 'https://maven.fabric.io/public' } } android { compileSdkVersion 30 buildToolsVersion "29.0.2" signingConfigs { release { try { storeFile file("../keystore test/testKey.jks") storePassword "testtest" keyAlias "test" keyPassword "admin@test" v2SigningEnabled true } catch (ex) { throw new InvalidUserDataException(ex.toString()) } } debug { storeFile file("../keystore test/debug.keystore") } } flavorDimensions "default", "color" productFlavors { flavor2 { dimension "default" println("@@@THIS IS FLAVOR2@@@") applicationId 'andresguard.tencent.com.andresguard_example.flavor2' } flavor1 { dimension "default" println("@@@THIS IS FLAVOR1@@@") applicationId 'andresguard.tencent.com.andresguard_example.flavor1' } aaa { dimension "default" println("@@@THIS IS aaa@@@") applicationId 'andresguard.tencent.com.andresguard_example.aaa' } red { dimension "color" println("@@@THIS IS red@@@") } blue { dimension "color" println("@@@THIS IS blue@@@") } } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' shrinkResources true zipAlignEnabled true pseudoLocalesEnabled true signingConfig signingConfigs.release } releaseLog { buildConfigField "boolean", "DEBUG_MODE", "false" buildConfigField "boolean", "LOG_DEBUG", "true" debuggable true minifyEnabled true zipAlignEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), "proguard-rules.pro" signingConfig signingConfigs.release matchingFallbacks = ['release'] } } defaultConfig { applicationId "andresguard.tencent.com.andresguard_example" minSdkVersion 24 targetSdkVersion 30 versionCode 3 versionName "1.2" } } task createVersionInformation { def resDir = new File(buildDir, 'generated/buildinfo/') def destDir = new File(resDir, 'META-INF/') android { sourceSets { main.resources { srcDir resDir } } } doLast { destDir.mkdirs() def vfile = new File(destDir, 'BUILD_INFO') def stdout = new ByteArrayOutputStream() exec { commandLine 'git', 'log', '-1', '--format=%H' standardOutput = stdout } def git_hash = stdout.toString().trim() stdout.reset() exec { commandLine 'git', 'config', 'remote.origin.url' standardOutput = stdout } def remote = stdout.toString().trim() def dirty = exec { commandLine 'git', 'diff-index', '--quiet', 'HEAD' ignoreExitValue true } dirty = dirty.getExitValue() == 0 ? ' (Clean workspace)' : '-DIRTY using dirty workspace!' def repo_info = "$remote at $git_hash$dirty" vfile.text = "$repo_info\n" } } project.afterEvaluate { tasks.findAll { task -> task.name.startsWith('merge') && task.name.endsWith('Resources') }.each { t -> t.dependsOn createVersionInformation } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.12' implementation project(':libres') implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support:design:26.1.0' // Crashlytics Kit implementation('com.crashlytics.sdk.android:crashlytics:2.7.1@aar') { transitive = true } // NDK Kit implementation('com.crashlytics.sdk.android:crashlytics-ndk:1.1.6@aar') { transitive = true } } ================================================ FILE: AndResGuard-example/app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/sun/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: AndResGuard-example/app/resource_mapping.txt ================================================ res path mapping: res/mipmap-hdpi-v4 -> res/mipmap-hdpi-v4 res/mipmap-mdpi-v4 -> res/mipmap-mdpi-v4 res/mipmap-xhdpi-v4 -> res/mipmap-xhdpi-v4 res/mipmap-xxhdpi-v4 -> res/mipmap-xxhdpi-v4 res/mipmap-xxxhdpi-v4 -> res/mipmap-xxxhdpi-v4 ================================================ FILE: AndResGuard-example/app/src/androidTest/java/andresguard/tencent/com/andresguard_example/ApplicationTest.java ================================================ package andresguard.tencent.com.andresguard_example; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: AndResGuard-example/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: AndResGuard-example/app/src/main/java/andresguard/tencent/com/andresguard_example/MainActivity.java ================================================ package andresguard.tencent.com.andresguard_example; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.view.View; import com.crashlytics.android.Crashlytics; import com.crashlytics.android.ndk.CrashlyticsNdk; import io.fabric.sdk.android.Fabric; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Fabric.with(this, new Crashlytics(), new CrashlyticsNdk()); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show(); } }); DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ); drawer.setDrawerListener(toggle); toggle.syncState(); NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); } @Override public void onBackPressed() { DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } @SuppressWarnings("StatementWithEmptyBody") @Override public boolean onNavigationItemSelected(MenuItem item) { // Handle navigation view item clicks here. int id = item.getItemId(); if (id == R.id.nav_camera) { // Handle the camera action } else if (id == R.id.nav_gallery) { } else if (id == R.id.nav_slideshow) { } else if (id == R.id.nav_manage) { } else if (id == R.id.nav_share) { } else if (id == R.id.nav_send) { } DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); drawer.closeDrawer(GravityCompat.START); return true; } } ================================================ FILE: AndResGuard-example/app/src/main/res/.keep ================================================ ================================================ FILE: AndResGuard-example/app/src/test/java/andresguard/tencent/com/andresguard_example/ExampleUnitTest.java ================================================ package andresguard.tencent.com.andresguard_example; import org.junit.Test; import static org.junit.Assert.assertEquals; /** * To work on unit tests, switch the Test Artifact in the Build Variants view. */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: AndResGuard-example/app1/.gitignore ================================================ /build ================================================ FILE: AndResGuard-example/app1/build.gradle ================================================ apply plugin: 'AndResGuard' apply plugin: 'com.android.application' buildscript { repositories { mavenLocal() jcenter() google() } dependencies { classpath("com.tencent.mm:AndResGuard-gradle-plugin:${ANDRESGUARD_VERSION}") { changing = true } } } andResGuard { mappingFile = file("./resource_mapping.txt") use7zip = true useSign = true keepRoot = false mergeDuplicatedRes = true // add .R.drawable.icon into whitelist. // because the launcher will get the icon with his name whiteList = ["R.mipmap.ic_launcher", //https://docs.fabric.io/android/crashlytics/build-tools.html "R.string.com.crashlytics.*", "R.id.*", "R.drawable.*", "R.layout.*"] compressFilePattern = ["*.png", "*.jpg", "*.jpeg", "*.gif", "resources.arsc"] sevenzip { artifact = "com.tencent.mm:SevenZip:${ANDRESGUARD_SEVENZIP_VERSION}" //path = "/usr/local/bin/7za" } } repositories { jcenter() maven { url 'https://maven.fabric.io/public' } } android { compileSdkVersion 26 buildToolsVersion '29.0.2' signingConfigs { release { try { storeFile file("../keystore test/release.keystore") storePassword "testres" keyAlias "testres" keyPassword "testres" v2SigningEnabled true } catch (ex) { throw new InvalidUserDataException(ex.toString()) } } debug { storeFile file("../keystore test/debug.keystore") } } // productFlavors { // flavor2 { // println("@@@THIS IS FLAVOR2@@@") // applicationId 'andresguard.tencent.com.andresguard_example.flavor2' // } // // flavor1 { // println("@@@THIS IS FLAVOR1@@@") // applicationId 'andresguard.tencent.com.andresguard_example.flavor1' // } // // aaa { // println("@@@THIS IS aaa@@@") // applicationId 'andresguard.tencent.com.andresguard_example.aaa' // } // } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' shrinkResources true zipAlignEnabled true pseudoLocalesEnabled true signingConfig signingConfigs.release } releaseLog { buildConfigField "boolean", "DEBUG_MODE", "false" buildConfigField "boolean", "LOG_DEBUG", "true" debuggable true minifyEnabled true zipAlignEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), "proguard-rules.pro" signingConfig signingConfigs.release matchingFallbacks = ['release'] } } defaultConfig { applicationId "andresguard.tencent.com.andresguard_example" minSdkVersion 14 targetSdkVersion 26 versionCode 2 versionName "1.1" } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.12' implementation project(':libres') implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support:design:26.1.0' // Crashlytics Kit implementation('com.crashlytics.sdk.android:crashlytics:2.7.1@aar') { transitive = true } // NDK Kit implementation('com.crashlytics.sdk.android:crashlytics-ndk:1.1.6@aar') { transitive = true } } ================================================ FILE: AndResGuard-example/app1/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/sun/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: AndResGuard-example/app1/resource_mapping.txt ================================================ res path mapping: res/mipmap-hdpi-v4 -> res/mipmap-hdpi-v4 res/mipmap-mdpi-v4 -> res/mipmap-mdpi-v4 res/mipmap-xhdpi-v4 -> res/mipmap-xhdpi-v4 res/mipmap-xxhdpi-v4 -> res/mipmap-xxhdpi-v4 res/mipmap-xxxhdpi-v4 -> res/mipmap-xxxhdpi-v4 ================================================ FILE: AndResGuard-example/app1/src/androidTest/java/andresguard/tencent/com/andresguard_example/ApplicationTest.java ================================================ package andresguard.tencent.com.andresguard_example; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: AndResGuard-example/app1/src/main/AndroidManifest.xml ================================================ ================================================ FILE: AndResGuard-example/app1/src/main/java/andresguard/tencent/com/andresguard_example/MainActivity.java ================================================ package andresguard.tencent.com.andresguard_example; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.view.View; import com.crashlytics.android.Crashlytics; import com.crashlytics.android.ndk.CrashlyticsNdk; import io.fabric.sdk.android.Fabric; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Fabric.with(this, new Crashlytics(), new CrashlyticsNdk()); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show(); } }); DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ); drawer.setDrawerListener(toggle); toggle.syncState(); NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); } @Override public void onBackPressed() { DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } @SuppressWarnings("StatementWithEmptyBody") @Override public boolean onNavigationItemSelected(MenuItem item) { // Handle navigation view item clicks here. int id = item.getItemId(); if (id == R.id.nav_camera) { // Handle the camera action } else if (id == R.id.nav_gallery) { } else if (id == R.id.nav_slideshow) { } else if (id == R.id.nav_manage) { } else if (id == R.id.nav_share) { } else if (id == R.id.nav_send) { } DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); drawer.closeDrawer(GravityCompat.START); return true; } } ================================================ FILE: AndResGuard-example/app1/src/main/res/.keep ================================================ ================================================ FILE: AndResGuard-example/app1/src/test/java/andresguard/tencent/com/andresguard_example/ExampleUnitTest.java ================================================ package andresguard.tencent.com.andresguard_example; import org.junit.Test; import static org.junit.Assert.assertEquals; /** * To work on unit tests, switch the Test Artifact in the Build Variants view. */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: AndResGuard-example/app2/.gitignore ================================================ /build ================================================ FILE: AndResGuard-example/app2/build.gradle ================================================ apply plugin: 'AndResGuard' apply plugin: 'com.android.application' buildscript { repositories { mavenLocal() jcenter() google() } dependencies { classpath("com.tencent.mm:AndResGuard-gradle-plugin:${ANDRESGUARD_VERSION}") { changing = true } } } andResGuard { mappingFile = file("./resource_mapping.txt") use7zip = true useSign = true keepRoot = false mergeDuplicatedRes = true // add .R.drawable.icon into whitelist. // because the launcher will get the icon with his name whiteList = ["R.mipmap.ic_launcher", //https://docs.fabric.io/android/crashlytics/build-tools.html "R.string.com.crashlytics.*", "R.id.*"] compressFilePattern = ["*.png", "*.jpg", "*.jpeg", "*.gif", "resources.arsc"] sevenzip { artifact = "com.tencent.mm:SevenZip:${ANDRESGUARD_SEVENZIP_VERSION}" //path = "/usr/local/bin/7za" } } repositories { jcenter() maven { url 'https://maven.fabric.io/public' } } android { compileSdkVersion 26 buildToolsVersion '29.0.2' signingConfigs { release { try { storeFile file("../keystore test/release.keystore") storePassword "testres" keyAlias "testres" keyPassword "testres" v2SigningEnabled true } catch (ex) { throw new InvalidUserDataException(ex.toString()) } } debug { storeFile file("../keystore test/debug.keystore") } } // productFlavors { // flavor2 { // println("@@@THIS IS FLAVOR2@@@") // applicationId 'andresguard.tencent.com.andresguard_example.flavor2' // } // // flavor1 { // println("@@@THIS IS FLAVOR1@@@") // applicationId 'andresguard.tencent.com.andresguard_example.flavor1' // } // // aaa { // println("@@@THIS IS aaa@@@") // applicationId 'andresguard.tencent.com.andresguard_example.aaa' // } // } buildTypes { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' shrinkResources true zipAlignEnabled true pseudoLocalesEnabled true signingConfig signingConfigs.release } releaseLog { buildConfigField "boolean", "DEBUG_MODE", "false" buildConfigField "boolean", "LOG_DEBUG", "true" debuggable true minifyEnabled true zipAlignEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), "proguard-rules.pro" signingConfig signingConfigs.release matchingFallbacks = ['release'] } } defaultConfig { applicationId "andresguard.tencent.com.andresguard_example" minSdkVersion 14 targetSdkVersion 26 versionCode 2 versionName "1.1" } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) testImplementation 'junit:junit:4.12' implementation project(':libres') implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support:design:26.1.0' // Crashlytics Kit implementation('com.crashlytics.sdk.android:crashlytics:2.7.1@aar') { transitive = true } // NDK Kit implementation('com.crashlytics.sdk.android:crashlytics-ndk:1.1.6@aar') { transitive = true } } ================================================ FILE: AndResGuard-example/app2/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/sun/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: AndResGuard-example/app2/resource_mapping.txt ================================================ res path mapping: res/mipmap-hdpi-v4 -> res/mipmap-hdpi-v4 res/mipmap-mdpi-v4 -> res/mipmap-mdpi-v4 res/mipmap-xhdpi-v4 -> res/mipmap-xhdpi-v4 res/mipmap-xxhdpi-v4 -> res/mipmap-xxhdpi-v4 res/mipmap-xxxhdpi-v4 -> res/mipmap-xxxhdpi-v4 ================================================ FILE: AndResGuard-example/app2/src/androidTest/java/andresguard/tencent/com/andresguard_example/ApplicationTest.java ================================================ package andresguard.tencent.com.andresguard_example; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: AndResGuard-example/app2/src/main/AndroidManifest.xml ================================================ ================================================ FILE: AndResGuard-example/app2/src/main/java/andresguard/tencent/com/andresguard_example/MainActivity.java ================================================ package andresguard.tencent.com.andresguard_example; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.view.View; import com.crashlytics.android.Crashlytics; import com.crashlytics.android.ndk.CrashlyticsNdk; import io.fabric.sdk.android.Fabric; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Fabric.with(this, new Crashlytics(), new CrashlyticsNdk()); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show(); } }); DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ); drawer.setDrawerListener(toggle); toggle.syncState(); NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); } @Override public void onBackPressed() { DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } @SuppressWarnings("StatementWithEmptyBody") @Override public boolean onNavigationItemSelected(MenuItem item) { // Handle navigation view item clicks here. int id = item.getItemId(); if (id == R.id.nav_camera) { // Handle the camera action } else if (id == R.id.nav_gallery) { } else if (id == R.id.nav_slideshow) { } else if (id == R.id.nav_manage) { } else if (id == R.id.nav_share) { } else if (id == R.id.nav_send) { } DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); drawer.closeDrawer(GravityCompat.START); return true; } } ================================================ FILE: AndResGuard-example/app2/src/main/res/.keep ================================================ ================================================ FILE: AndResGuard-example/app2/src/test/java/andresguard/tencent/com/andresguard_example/ExampleUnitTest.java ================================================ package andresguard.tencent.com.andresguard_example; import org.junit.Test; import static org.junit.Assert.assertEquals; /** * To work on unit tests, switch the Test Artifact in the Build Variants view. */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: AndResGuard-example/build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { google() jcenter() } } ================================================ FILE: AndResGuard-example/gradle/wrapper/gradle-wrapper.properties ================================================ #Fri May 29 18:04:51 PDT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip ================================================ FILE: AndResGuard-example/gradle.properties ================================================ ## Project-wide Gradle settings. # # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx1024m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true #Tue Sep 06 04:33:31 CST 2016 org.gradle.jvmargs=-Xmx2584M org.gradle.parallel=true org.gradle.daemon=true ANDRESGUARD_VERSION=1.2.20 ANDRESGUARD_SEVENZIP_VERSION=1.2.20 ================================================ FILE: AndResGuard-example/gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: AndResGuard-example/gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: AndResGuard-example/libres/.gitignore ================================================ /build ================================================ FILE: AndResGuard-example/libres/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion 26 buildToolsVersion '29.0.2' defaultConfig { minSdkVersion 14 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { api fileTree(dir: 'libs', include: ['*.jar']) androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) api 'com.android.support:appcompat-v7:26.1.0' api 'com.android.support:design:26.1.0' testImplementation 'junit:junit:4.12' } ================================================ FILE: AndResGuard-example/libres/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Users/sun/Library/Android/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: AndResGuard-example/libres/src/androidTest/java/com/tinkerpatch/libres/ExampleInstrumentedTest.java ================================================ package com.tinkerpatch.libres; import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; import static org.junit.Assert.*; /** * Instrumentation test, which will execute on an Android device. * * @see Testing documentation */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { @Test public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getTargetContext(); assertEquals("com.tinkerpatch.libres.test", appContext.getPackageName()); } } ================================================ FILE: AndResGuard-example/libres/src/main/AndroidManifest.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/drawable/side_nav_bar.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/drawable-v21/ic_menu_camera.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/drawable-v21/ic_menu_gallery.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/drawable-v21/ic_menu_manage.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/drawable-v21/ic_menu_send.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/drawable-v21/ic_menu_share.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/drawable-v21/ic_menu_slideshow.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/layout/app_bar_main.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/layout/content_main.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/layout/nav_header_main.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/menu/activity_main_drawer.xml ================================================

================================================ FILE: AndResGuard-example/libres/src/main/res/menu/main.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/values/colors.xml ================================================ #3F51B5 #303F9F #FF4081 ================================================ FILE: AndResGuard-example/libres/src/main/res/values/dimens.xml ================================================ 16dp 160dp 16dp 16dp 16dp ================================================ FILE: AndResGuard-example/libres/src/main/res/values/drawables.xml ================================================ @android:drawable/ic_menu_camera @android:drawable/ic_menu_gallery @android:drawable/ic_menu_slideshow @android:drawable/ic_menu_manage @android:drawable/ic_menu_share @android:drawable/ic_menu_send ================================================ FILE: AndResGuard-example/libres/src/main/res/values/strings.xml ================================================ AndResGuard-example Open navigation drawer Close navigation drawer Settings ================================================ FILE: AndResGuard-example/libres/src/main/res/values/styles.xml ================================================ ================================================ FILE: AndResGuard-example/libres/src/main/res/values-w820dp/dimens.xml ================================================ 64dp ================================================ FILE: AndResGuard-example/libres/src/main/res/values-w820dp/strings.xml ================================================ AndResGuard-example 820 Open navigation drawer 820 Close navigation drawer 820 Settings 820 ================================================ FILE: AndResGuard-example/libres/src/main/res/values-xxhdpi/strings.xml ================================================ AndResGuard-example xxh Open navigation drawer xxh Close navigation drawer xxh Settings xxh ================================================ FILE: AndResGuard-example/libres/src/test/java/com/tinkerpatch/libres/ExampleUnitTest.java ================================================ package com.tinkerpatch.libres; import org.junit.Test; import static org.junit.Assert.*; /** * Example local unit test, which will execute on the development machine (host). * * @see Testing documentation */ public class ExampleUnitTest { @Test public void addition_isCorrect() throws Exception { assertEquals(4, 2 + 2); } } ================================================ FILE: AndResGuard-example/settings.gradle ================================================ include ':app', ':libres', ':app1', ':app2' ================================================ FILE: AndResGuard-gradle-plugin/build.gradle ================================================ apply plugin: 'groovy' version rootProject.ext.VERSION_NAME group rootProject.ext.GROUP dependencies { compile gradleApi() compile localGroovy() //compile group: 'com.tencent.mm', name: 'AndResGuard-core', version: version compile 'com.google.gradle:osdetector-gradle-plugin:1.6.0' compile project(':AndResGuard-core') } repositories { mavenCentral() } apply from: rootProject.file('gradle/java-artifacts.gradle') apply from: rootProject.file('gradle/gradle-mvn-push.gradle') ================================================ FILE: AndResGuard-gradle-plugin/gradle.properties ================================================ POM_ARTIFACT_ID=AndResGuard-gradle-plugin POM_NAME=AndResGuard Gradle Plugin POM_PACKAGING=jar ================================================ FILE: AndResGuard-gradle-plugin/src/main/groovy/com/tencent/gradle/AndResGuardExtension.groovy ================================================ package com.tencent.gradle /** * The configuration properties. * * @author sim sun (sunsj1231@gmail.com) */ class AndResGuardExtension { File mappingFile boolean use7zip boolean useSign String metaName String fixedResName boolean keepRoot boolean mergeDuplicatedRes Iterable whiteList Iterable compressFilePattern String finalApkBackupPath String digestalg String sourceApk String sourceBuildType String sourceFlavor AndResGuardExtension() { use7zip = false useSign = false metaName = "META-INF" fixedResName = null keepRoot = false mergeDuplicatedRes = false whiteList = [] compressFilePattern = [] mappingFile = null finalApkBackupPath = null digestalg = null sourceApk = null sourceBuildType = null sourceFlavor = null } Iterable getCompressFilePattern() { return compressFilePattern } File getMappingFile() { return mappingFile } boolean getUse7zip() { return use7zip } boolean getUseSign() { return useSign } String getMetaName() { return metaName } String getFixedResName() { return fixedResName } boolean getKeepRoot() { return keepRoot } boolean getMergeDuplicatedRes() { return mergeDuplicatedRes } Iterable getWhiteList() { return whiteList } String getFinalApkBackupPath() { return finalApkBackupPath } String getDigestalg() { return digestalg } String getSourceApk() { return sourceApk } String getSourceBuildType() { return sourceBuildType } String getSourceFlavor() { return sourceFlavor } @Override String toString() { """| use7zip = ${use7zip} | useSign = ${useSign} | metaName = ${metaName} | fixedResName = ${fixedResName} | keepRoot = ${keepRoot} | mergeDuplicatedRes = ${mergeDuplicatedRes} | whiteList = ${whiteList} | compressFilePattern = ${compressFilePattern} | finalApkBackupPath = ${finalApkBackupPath} | digstalg = ${digestalg} | sourceApk = ${sourceApk} | sourceBuildType = ${sourceBuildType} | sourceFlavor = ${sourceFlavor} """.stripMargin() } } ================================================ FILE: AndResGuard-gradle-plugin/src/main/groovy/com/tencent/gradle/AndResGuardPlugin.groovy ================================================ package com.tencent.gradle import org.gradle.api.Plugin import org.gradle.api.Project /** * Registers the plugin's tasks. * * @author sim sun (sunsj1231@gmail.com) */ class AndResGuardPlugin implements Plugin { public static final String USE_APK_TASK_NAME = "UseApk" @Override void apply(Project project) { project.apply plugin: 'com.google.osdetector' project.extensions.create('andResGuard', AndResGuardExtension) project.extensions.add("sevenzip", new ExecutorExtension("sevenzip")) project.afterEvaluate { def android = project.extensions.android createTask(project, USE_APK_TASK_NAME) android.applicationVariants.all { variant -> def variantName = variant.name.capitalize() createTask(project, variantName) } android.buildTypes.all { buildType -> def buildTypeName = buildType.name.capitalize() createTask(project, buildTypeName) } android.productFlavors.all { flavor -> def flavorName = flavor.name.capitalize() createTask(project, flavorName) } project.extensions.findByName("sevenzip").loadArtifact(project) } } private static void createTask(Project project, variantName) { def taskName = "resguard${variantName}" if (project.tasks.findByPath(taskName) == null) { def task = project.task(taskName, type: AndResGuardTask) if (variantName != USE_APK_TASK_NAME) { task.dependsOn "assemble${variantName}" } } } } ================================================ FILE: AndResGuard-gradle-plugin/src/main/groovy/com/tencent/gradle/AndResGuardTask.groovy ================================================ package com.tencent.gradle import com.tencent.mm.androlib.res.util.StringUtil import com.tencent.mm.directory.PathNotExist import com.tencent.mm.resourceproguard.InputParam import com.tencent.mm.resourceproguard.Main import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.provider.Property import org.gradle.api.tasks.TaskAction /** * The configuration properties. * * @author Sim Sun (sunsj1231@gmail.com) */ class AndResGuardTask extends DefaultTask { AndResGuardExtension configuration def android def buildConfigs = [] AndResGuardTask() { description = 'Assemble Resource Proguard APK' group = 'andresguard' outputs.upToDateWhen { false } android = project.extensions.android configuration = project.andResGuard if (StringUtil.isPresent(configuration.digestalg) && !configuration.digestalg.contains('-')) { throw new RuntimeException("Plz add - in your digestalg, such as SHA-1 SHA-256") } android.applicationVariants.all { variant -> variant.outputs.each { output -> // remove "resguard" String variantName = this.name["resguard".length()..-1] if (variantName.equalsIgnoreCase(variant.buildType.name as String) || isTargetFlavor(variantName, variant.productFlavors, variant.buildType.name) || variantName.equalsIgnoreCase(AndResGuardPlugin.USE_APK_TASK_NAME)) { def outputFile = null try { if (variant.metaClass.respondsTo(variant, "getPackageApplicationProvider")) { outputFile = new File(variant.packageApplicationProvider.get().outputDirectory, output.outputFileName) } } catch (Exception ignore) { // no-op } finally { outputFile = outputFile ?: output.outputFile } def variantInfo if (variant.variantData.hasProperty("variantConfiguration")) { variantInfo = variant.variantData.variantConfiguration } else { variantInfo = variant.variantData.variantDslInfo } def applicationId = variantInfo.applicationId instanceof Property ? variantInfo.applicationId.get() : variantInfo.applicationId buildConfigs << new BuildInfo( outputFile, variantInfo.signingConfig, applicationId, variant.buildType.name, variant.productFlavors, variantName, variant.mergedFlavor.minSdkVersion.apiLevel, variant.mergedFlavor.targetSdkVersion.apiLevel, ) } } } if (!project.plugins.hasPlugin('com.android.application')) { throw new GradleException('generateARGApk: Android Application plugin required') } } static isTargetFlavor(variantName, flavors, buildType) { if (flavors.size() > 0) { String flavor = flavors.get(0).name return variantName.equalsIgnoreCase(flavor) || variantName.equalsIgnoreCase([flavors.collect { it.name }.join(""), buildType].join("")) } return false } static useFolder(file) { //remove .apk from filename def fileName = file.name[0..-5] return "${file.parent}/AndResGuard_${fileName}/" } def getZipAlignPath() { return "${android.getSdkDirectory().getAbsolutePath()}/build-tools/${android.buildToolsVersion}/zipalign" } @TaskAction run() { project.logger.info("[AndResGuard] configuartion:$configuration") project.logger.info("[AndResGuard] BuildConfigs:$buildConfigs") buildConfigs.each { config -> if (config.taskName == AndResGuardPlugin.USE_APK_TASK_NAME) { if (StringUtil.isBlank(configuration.sourceApk) || !new File(configuration.sourceApk).exists()) { throw new PathNotExist("Original APK not existed for " + AndResGuardPlugin.USE_APK_TASK_NAME) } if (config.flavors.productFlavors.size() > 0 && StringUtil.isBlank(configuration.sourceFlavor)) { throw new RuntimeException("Must setup sourceFlavor when flavors exist in build.gradle") } if (StringUtil.isBlank(configuration.sourceBuildType)) { throw new RuntimeException("Must setup sourceBuildType when flavors exist in build.gradle") } if (config.buildType == configuration.sourceBuildType) { if (StringUtil.isBlank(configuration.sourceFlavor) || (StringUtil.isPresent(configuration.sourceFlavor) && config.flavors.size() > 0 && config.flavors.get(0).name == configuration.sourceFlavor)) { RunGradleTask(config, configuration.sourceApk, config.minSDKVersion, config.targetSDKVersion) } } } else { if (config.file == null || !config.file.exists()) { throw new PathNotExist("Original APK not existed") } RunGradleTask(config, config.file.getAbsolutePath(), config.minSDKVersion, config.targetSDKVersion) } } } def RunGradleTask(config, String absPath, int minSDKVersion, int targetSDKVersion) { def signConfig = config.signConfig String packageName = config.packageName ArrayList whiteListFullName = new ArrayList<>() ExecutorExtension sevenzip = project.extensions.findByName("sevenzip") as ExecutorExtension configuration.whiteList.each { res -> if (res.startsWith("R")) { whiteListFullName.add(packageName + "." + res) } else { whiteListFullName.add(res) } } InputParam.Builder builder = new InputParam.Builder() .setMappingFile(configuration.mappingFile) .setWhiteList(whiteListFullName) .setUse7zip(configuration.use7zip) .setMetaName(configuration.metaName) .setFixedResName(configuration.fixedResName) .setKeepRoot(configuration.keepRoot) .setMergeDuplicatedRes(configuration.mergeDuplicatedRes) .setCompressFilePattern(configuration.compressFilePattern) .setZipAlign(getZipAlignPath()) .setSevenZipPath(sevenzip.path) .setOutBuilder(useFolder(config.file)) .setApkPath(absPath) .setUseSign(configuration.useSign) .setDigestAlg(configuration.digestalg) .setMinSDKVersion(minSDKVersion) .setTargetSDKVersion(targetSDKVersion) if (configuration.finalApkBackupPath != null && configuration.finalApkBackupPath.length() > 0) { builder.setFinalApkBackupPath(configuration.finalApkBackupPath) } else { builder.setFinalApkBackupPath(absPath) } if (configuration.useSign) { if (signConfig == null) { throw new GradleException("can't the get signConfig for release build") } builder.setSignFile(signConfig.storeFile) .setKeypass(signConfig.keyPassword) .setStorealias(signConfig.keyAlias) .setStorepass(signConfig.storePassword) if (signConfig.hasProperty('v3SigningEnabled') && signConfig.v3SigningEnabled) { builder.setSignatureType(InputParam.SignatureType.SchemaV3) } else if (signConfig.hasProperty('v2SigningEnabled') && signConfig.v2SigningEnabled) { builder.setSignatureType(InputParam.SignatureType.SchemaV2) } } InputParam inputParam = builder.create() Main.gradleRun(inputParam) } } ================================================ FILE: AndResGuard-gradle-plugin/src/main/groovy/com/tencent/gradle/BuildInfo.groovy ================================================ package com.tencent.gradle /** * Created by simsun on 5/13/16.*/ class BuildInfo { def file def signConfig def packageName def buildType def flavors def taskName int minSDKVersion int targetSDKVersion BuildInfo(file, sign, packageName, buildType, flavors, taskName, minSDKVersion, targetSDKVersion) { this.file = file this.signConfig = sign this.packageName = packageName this.buildType = buildType this.flavors = flavors this.taskName = taskName this.minSDKVersion = minSDKVersion this.targetSDKVersion = targetSDKVersion } @Override String toString() { """| file = ${file} | packageName = ${packageName} | buildType = ${buildType} | flavors = ${flavors} | taskname = ${taskName} | minSDKVersion = ${minSDKVersion} | targetSDKVersion = ${targetSDKVersion} """.stripMargin() } } ================================================ FILE: AndResGuard-gradle-plugin/src/main/groovy/com/tencent/gradle/ExecutorExtension.groovy ================================================ package com.tencent.gradle import org.gradle.api.GradleException import org.gradle.api.Named import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.Dependency class ExecutorExtension implements Named { private final String name private String artifact private String path ExecutorExtension(String name) { this.name = name } @Override String getName() { return name } /** * Specifies an artifact spec for downloading the executable from * repositories. spec format: '::'*/ def setArtifact(String spec) { this.artifact = spec } /** * Specifies a local path.*/ def setPath(String path) { this.path = path } String getArtifact() { return artifact } String getPath() { return path } void loadArtifact(Project project) { if (path == null && artifact != null) { Configuration config = project.configurations.create("AndResGuardLocatorSevenZip") { visible = false transitive = false extendsFrom = [] } def groupId, artifactId, version (groupId, artifactId, version) = this.artifact.split(":") def notation = [group : groupId, name : artifactId, version : version, classifier: project.osdetector.classifier, ext : 'exe'] project.logger.info("[AndResGuard]Resolving artifact: ${notation}") Dependency dep = project.dependencies.add(config.name, notation) File file = config.fileCollection(dep).singleFile if (!file.canExecute() && !file.setExecutable(true)) { throw new GradleException("Cannot set ${file} as executable") } project.logger.info("[AndResGuard]Resolved artifact: ${file}") this.path = file.path } } } ================================================ FILE: AndResGuard-gradle-plugin/src/main/resources/META-INF/gradle-plugins/AndResGuard.properties ================================================ implementation-class=com.tencent.gradle.AndResGuardPlugin ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # AndResGuard [![Build Status](https://travis-ci.org/shwenzhang/AndResGuard.svg?branch=master)](https://travis-ci.org/shwenzhang/AndResGuard) [ ![Download](https://api.bintray.com/packages/wemobiledev/maven/com.tencent.mm%3AAndResGuard-core/images/download.svg) ](https://bintray.com/wemobiledev/maven/com.tencent.mm%3AAndResGuard-core/_latestVersion) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndResGuard-green.svg?style=true)](https://android-arsenal.com/details/1/3034) *Read this in other languages: [English](README.md), [简体中文](README.zh-cn.md).* `AndResGuard` is a tooling for reducing your apk size, it works like the `ProGuard` for Java source code, but only aim at the resource files. It changes `res/drawable/wechat` to `r/d/a`, and renames the resource file `wechat.png` to `a.png`. Finally, it repackages the apk with 7zip, which can reduce the package size obviously. `AndResGuard` is fast, and it does **NOT** need the source codes. Input an Android apk, then we can get a 'ResGuard' apk in a few seconds. Some uses of `AndResGuard` are: 1. Obfuscate android resources. It contains all the resource type(such as drawable、layout、string...). It can prevent your apk from being reversed by `Apktool`. 2. Shrinking the apk size. It can reduce the `resources.arsc` and the package size obviously. 3. Repackage with `7zip`. It supports repackage apk with `7zip`, and we can specify the compression method for each file. `AndResGuard` is a command-line tool, it supports Windows, Linux and Mac. We suggest you to use 7zip in Linux or Mac platform for a higher compression ratio. ## How to use ### With Gradle This has been released on `Bintray` ```gradle apply plugin: 'AndResGuard' buildscript { repositories { jcenter() google() } dependencies { classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21' } } andResGuard { // mappingFile = file("./resource_mapping.txt") mappingFile = null use7zip = true useSign = true // It will keep the origin path of your resources when it's true keepRoot = false // If set, name column in arsc those need to proguard will be kept to this value fixedResName = "arg" // It will merge the duplicated resources, but don't rely on this feature too much. // it's always better to remove duplicated resource from repo mergeDuplicatedRes = true whiteList = [ // your icon "R.drawable.icon", // for fabric "R.string.com.crashlytics.*", // for google-services "R.string.google_app_id", "R.string.gcm_defaultSenderId", "R.string.default_web_client_id", "R.string.ga_trackingId", "R.string.firebase_database_url", "R.string.google_api_key", "R.string.google_crash_reporting_api_key", "R.string.project_id", ] compressFilePattern = [ "*.png", "*.jpg", "*.jpeg", "*.gif", ] sevenzip { artifact = 'com.tencent.mm:SevenZip:1.2.21' //path = "/usr/local/bin/7za" } /** * Optional: if finalApkBackupPath is null, AndResGuard will overwrite final apk * to the path which assemble[Task] write to **/ // finalApkBackupPath = "${project.rootDir}/final.apk" /** * Optional: Specifies the name of the message digest algorithm to user when digesting the entries of JAR file * Only works in V1signing, default value is "SHA-1" **/ // digestalg = "SHA-256" } ``` ### Wildcard The whiteList and compressFilePattern support wildcard include ? * +. ``` ? Zero or one character * Zero or more of character + One or more of character ``` ### WhiteList You need put all resource which access via `getIdentifier` into whiteList. **You can find more whitsList configs of third-part SDK in [white_list.md](doc/white_list.md). Welcome PR your configs which is not included in white_list.md** The whiteList only works on the specsName of resources, it wouldn't keep the path of resource. If you wanna keeping the path, please use `mappingFile` to implement it. For example, we wanna keeping the path of icon, we need add below into our `mappingFile`. ``` res path mapping: res/mipmap-hdpi-v4 -> res/mipmap-hdpi-v4 res/mipmap-mdpi-v4 -> res/mipmap-mdpi-v4 res/mipmap-xhdpi-v4 -> res/mipmap-xhdpi-v4 res/mipmap-xxhdpi-v4 -> res/mipmap-xxhdpi-v4 res/mipmap-xxxhdpi-v4 -> res/mipmap-xxxhdpi-v4 ``` ### How to Launch If you are using `Android Studio`, you can find the generate task option in ```andresguard``` group. Or alternatively, you run ```./gradlew resguard[BuildType | Flavor]``` in your terminal. The format of task name is as same as `assemble`. ### Sevenzip The `sevenzip` in gradle file can be set by `path` or `artifact`. Multiple assignments are allowed, but the winner is **always** `path`. ### Result If finalApkBackupPath is null, AndResGuard will overwrite final APK to the path which assemble[Task] write. Otherwise, it will store in the path you assigned. ### Other [Looking for more detail](doc/how_to_work.md) ## Known Issue 1. The first element of list which returned by `AssetManager#list(String path)` is empty string when you're using the APK which is compressed by 7zip. [#162](https://github.com/shwenzhang/AndResGuard/issues/162) ## Best Practise 1. Do **NOT** add `resources.arsc` into `compressFilePattern` unless the app size is really matter to you.([#84](https://github.com/shwenzhang/AndResGuard/issues/84) [#233](https://github.com/shwenzhang/AndResGuard/issues/233)) 2. Do **NOT** enable 7zip compression(`use7zip`) when you distribute your APP on Google Play. It'll prevent the file-by-file patch when updating your APP. ([#233](https://github.com/shwenzhang/AndResGuard/issues/233)) ## Thanks [Apktool](https://github.com/iBotPeaches/Apktool) Connor Tumbleson [v2sig](https://github.com/shwenzhang/AndResGuard/pull/133) @jonyChina162 ================================================ FILE: README.zh-cn.md ================================================ # Android资源混淆工具使用说明 # [![Build Status](https://travis-ci.org/shwenzhang/AndResGuard.svg?branch=master)](https://travis-ci.org/shwenzhang/AndResGuard) [ ![Download](https://api.bintray.com/packages/wemobiledev/maven/com.tencent.mm%3AAndResGuard-core/images/download.svg) ](https://bintray.com/wemobiledev/maven/com.tencent.mm%3AAndResGuard-core/_latestVersion) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-AndResGuard-green.svg?style=true)](https://android-arsenal.com/details/1/3034) *其他语言版本: [English](README.md), [简体中文](README.zh-cn.md).* `AndResGuard`是一个帮助你缩小APK大小的工具,他的原理类似Java Proguard,但是只针对资源。他会将原本冗长的资源路径变短,例如将`res/drawable/wechat`变为`r/d/a`。 `AndResGuard`不涉及编译过程,只需输入一个apk(无论签名与否,debug版,release版均可,在处理过程中会直接将原签名删除),可得到一个实现资源混淆后的apk(若在配置文件中输入签名信息,可自动重签名并对齐,得到可直接发布的apk)以及对应资源ID的mapping文件。 原理介绍:[详见WeMobileDev公众号文章](http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=208135658&idx=1&sn=ac9bd6b4927e9e82f9fa14e396183a8f#rd) ## 使用Gradle 此工具已发布在Bintray ```gradle apply plugin: 'AndResGuard' buildscript { repositories { jcenter() google() } dependencies { classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.21' } } andResGuard { // mappingFile = file("./resource_mapping.txt") mappingFile = null use7zip = true useSign = true // 打开这个开关,会keep住所有资源的原始路径,只混淆资源的名字 keepRoot = false // 设置这个值,会把arsc name列混淆成相同的名字,减少string常量池的大小 fixedResName = "arg" // 打开这个开关会合并所有哈希值相同的资源,但请不要过度依赖这个功能去除去冗余资源 mergeDuplicatedRes = true whiteList = [ // for your icon "R.drawable.icon", // for fabric "R.string.com.crashlytics.*", // for google-services "R.string.google_app_id", "R.string.gcm_defaultSenderId", "R.string.default_web_client_id", "R.string.ga_trackingId", "R.string.firebase_database_url", "R.string.google_api_key", "R.string.google_crash_reporting_api_key" ] compressFilePattern = [ "*.png", "*.jpg", "*.jpeg", "*.gif", ] sevenzip { artifact = 'com.tencent.mm:SevenZip:1.2.21' //path = "/usr/local/bin/7za" } /** * 可选: 如果不设置则会默认覆盖assemble输出的apk **/ // finalApkBackupPath = "${project.rootDir}/final.apk" /** * 可选: 指定v1签名时生成jar文件的摘要算法 * 默认值为“SHA-1” **/ // digestalg = "SHA-256" } ``` ### 文件通配符 compressFilePattern和compressFilePattern中的通配符支持? + * ``` ? Zero or one character * Zero or more of character + One or more of character ``` ### 白名单 所有使用`getIdentifier`访问的资源都需要加入白名单。 **请使用Umeng_social_sdk的同学特别留意将资源加入白名单,否则会出现Crash。可以在[white_list.md](doc/white_list.md)查看更多sdk的白名单配置,也欢迎大家PR自己的白名单** 白名单机制只作用于资源的specsName,不会keep住资源的路径。如果想keep住资源原有的物理路径,可以使用`mappingFile`。 例如我想keep住icon所有folder,可以在mappingFile指向的文件添加: ``` res path mapping: res/mipmap-hdpi-v4 -> res/mipmap-hdpi-v4 res/mipmap-mdpi-v4 -> res/mipmap-mdpi-v4 res/mipmap-xhdpi-v4 -> res/mipmap-xhdpi-v4 res/mipmap-xxhdpi-v4 -> res/mipmap-xxhdpi-v4 res/mipmap-xxxhdpi-v4 -> res/mipmap-xxxhdpi-v4 ``` ### 如何启动 使用Android Studio的同学可以再 `andresguard` 下找到相关的构建任务; 命令行可直接运行```./gradlew resguard[BuildType | Flavor]```, 这里的任务命令规则和assemble一致。 ### 配置7Zip 在设置`sevenzip`时, 你只需设置`artifact`或`path`. 支持同时设置,总以path的值为优先. ### 结果 如果没有配置`finalApkBackupPath`,最终结果会覆盖`assemble[BuildType | Flavor]`的输出APK。如果配置则输出至`finalApkBackupPath`配置路径。 ### 其他 [点击查看更多细节和命令行使用方法](doc/how_to_work.zh-cn.md) ## 已知问题 1. 当时在使用7zip压缩的APK时,调用`AssetManager#list(String path)`返回结果的首个元素为空字符串. [#162](https://github.com/shwenzhang/AndResGuard/issues/162) ## 最佳实践 1. 如果不是对APK size有极致的需求,请不要把`resources.arsc`添加进`compressFilePattern`. ([#84](https://github.com/shwenzhang/AndResGuard/issues/84) [#233](https://github.com/shwenzhang/AndResGuard/issues/233)) 2. 对于发布于Google Play的APP,建议不要使用7Zip压缩,因为这个会导致Google Play的优化Patch算法失效. ([#233](https://github.com/shwenzhang/AndResGuard/issues/233)) ## 致谢 [Apktool](https://github.com/iBotPeaches/Apktool) 使用了Apktool资源解码部分的代码 [v2sig](https://github.com/shwenzhang/AndResGuard/pull/133) @jonyChina162 ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions Use this section to tell people about which versions of your project are currently being supported with security updates. | Version | Supported | | ------- | ------------------ | | 5.1.x | :white_check_mark: | | 5.0.x | :x: | | 4.0.x | :white_check_mark: | | < 4.0 | :x: | ## Reporting a Vulnerability Use this section to tell people how to report a vulnerability. Tell them where to go, how often they can expect to get an update on a reported vulnerability, what to expect if the vulnerability is accepted or declined, etc. ================================================ FILE: SevenZip/build.gradle ================================================ //apply plugin: 'com.google.osdetector' apply plugin: 'maven' apply plugin: 'maven-publish' apply plugin: 'java' version rootProject.ext.VERSION_NAME group rootProject.ext.GROUP task sourcesJar(type: Jar) { from sourceSets.main.java.srcDirs classifier = 'sources' } task javadocJar(type: Jar, dependsOn: javadoc) { classifier = 'javadoc' from javadoc.destinationDir } artifacts { archives javadocJar archives sourcesJar } publishing { publications { ResguardPub(MavenPublication) { artifactId POM_ARTIFACT_ID groupId group artifact("executable/SevenZip-linux-x86_32.exe") { classifier "linux-x86_32" extension "exe" } artifact("executable/SevenZip-linux-x86_64.exe") { classifier "linux-x86_64" extension "exe" } artifact("executable/SevenZip-windows-x86_32.exe") { classifier "windows-x86_32" extension "exe" } artifact("executable/SevenZip-windows-x86_64.exe") { classifier "windows-x86_64" extension "exe" } artifact("executable/SevenZip-osx-x86_64.exe") { classifier "osx-x86_64" extension "exe" } } } } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') ================================================ FILE: SevenZip/gradle.properties ================================================ POM_ARTIFACT_ID=SevenZip POM_NAME=Seven Zip ================================================ FILE: appveyol.yml ================================================ version: '{build}' skip_tags: true skip_commits: message: /\[ci skip\]/ clone_depth: 10 environment: TERM: dumb matrix: - JAVA_HOME: C:\Program Files\Java\jdk1.7.0 - JAVA_HOME: C:\Program Files\Java\jdk1.8.0 install: # prepend Java entry, remove Ruby entry (C:\Ruby193\bin;) from PATH - SET PATH=%JAVA_HOME%\bin;%PATH:C:\Ruby193\bin;=% - echo %PATH% - gradlew.bat --version build_script: - gradlew.bat -u -i clean assemble test_script: - gradlew.bat -u -i -S check cache: - .gradle - C:\Users\appveyor\.gradle on_failure: - echo Somebody setup us the bomb ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4' classpath 'com.ofg:uptodate-gradle-plugin:1.6.2' } } allprojects { repositories { google() jcenter() } tasks.withType(JavaCompile) { sourceCompatibility = rootProject.ext.javaVersion targetCompatibility = rootProject.ext.javaVersion } tasks.withType(GroovyCompile) { sourceCompatibility = rootProject.ext.javaVersion targetCompatibility = rootProject.ext.javaVersion } } ext { javaVersion = JavaVersion.VERSION_1_8 GROUP = 'com.tencent.mm' VERSION_NAME = "${ANDRESGUARD_VESSION}" POM_PACKAGING = "pom" POM_DESCRIPTION = "Android Resource Proguard Core Lib" POM_URL = "https://github.com/shwenzhang/AndResGuard" POM_SCM_URL = "https://github.com/shwenzhang/AndResGuard.git" POM_ISSUE_URL = 'https://github.com/shwenzhang/AndResGuard/issues' POM_LICENCE_NAME = "Apache-2.0" POM_LICENCE_URL = " http://www.apache.org/licenses/" POM_LICENCE_DIST = "repo" POM_DEVELOPER_ID = "Tencent Wechat" POM_DEVELOPER_NAME = "Tencent Wechat, Inc." BINTRAY_LICENCE = ["Apache-2.0"] BINTRAY_ORGANIZATION = "wemobiledev" } ================================================ FILE: doc/how_to_work.md ================================================ ### With Command Line ``` java -jar andresguard-x.x.x.jar -h ``` **You can find a simple example in `tools_output` folder.** we can see the help description, The easiest way is : `java -jar andresguard.jar input.apk`. Then it would try to read the config,xml, and output the results to the directory with the name of input.apk. - -config, set the config file yourself, if not, the default path is the running location with name config.xml. - -out, set the output directory yourself, if not, the default directory is the running location with name of the input file - -signature, set sign property, following by parameters: signature_file_path storepass keypass storealias, if you set these, the sign data in the config file will be overlayed. - -mapping, set keep mapping property, following by parameters: mapping_file_path. if you set these, the mapping data in the config file will be overlayed. - -7zip, set the 7zip path, such as /home/shwenzhang/tools/7za, window will be end of 7za.exe. > Window: > set 7za to environment variables。Address: [http://sparanoid.com/lab/7z/download.html](http://sparanoid.com/lab/7z/download.html) > > linux: sudo apt-get install p7zip-full > > mac: brew install p7zip - -zipalign, set the zipalign, such as /home/shwenzhang/sdk/tools/zipalign, window will be end of zipalign.exe. - -repackage, usually, when we build the channeles apk, it may destroy the 7zip. so you may need to use 7zip to repackage the apk ![](http://i.imgur.com/FssA59a.jpg) **2.samples** java -jar resourceproguard.jar input.apk if you want to special the output path or config file path, you can input: java -jar resourceproguard.jar input.apk -config yourconfig.xml -out output_directory if you want to special the sign or mapping data, you can input: java -jar resourceproguard.jar input.apk -config yourconfig.xml -out output_directory -signature signature_file_path storepass_value keypass_value storealias_value -mapping mapping_file_path if you want to special 7za or zipalign path, you can input: java -jar resourceproguard.jar input.apk -7zip /shwenzhang/tool/7za -zipalign /shwenzhang/sdk/tools/zipalign if you just want to repackage an apk compress with 7z: java -jar resourceproguard.jar -repackage input.apk -out output_directory -7zip /shwenzhang/tool/7za -zipalign /shwenzhang/sdk/tools/zipalign ### What we get Normally, we can get the following 7 useful files: ![](http://i.imgur.com/LtzSGC4.png) During the process, we can see the cost time and the reduce size. ![](http://i.imgur.com/ICDkJCH.png) ### How to write config.xml file There are five main configurations:property, whitelist, keepmapping, compress, sign。 **1. Property** Common properties: - --sevenzip, whether use 7zip to repackage the signed apk, you must install the 7z command line version first. - --metaname, the sign data file name in your apk, default must be META-INF. - --keeproot, if keep root, res/drawable will be kept, it won't be changed to such as r/s. ![](http://i.imgur.com/JfkZ09e.gif) **2. Whitelist** Whitelist property is used for keeping the resource you want. Because some resource id you can not proguard, such as throug method getIdentifier. - --isactive, whether to use whitelist, you can set false to close it simply. - --path, you must write the full package name, such as com.tencent.mm.R.drawable.icon. For some reason, we should keep our icon better, and it support *, ?, such as com.tencent.mm.R.drawable.emoji_* or com.tencent.mm.R.drawable.emoji_? Warning:1. donot write the file format name, such com.tencent.mm.R.drawable.emoji.png;2. * mean .+, a* would not match a; ![](http://i.imgur.com/VZ4fOa2.gif) **3. Keepmapping** sometimes if we want to keep the last way of obfuscation, we can use keepmapping mode. It is just like applymapping in ProGuard. - --isactive, whether to use keepmapping, you can set false to close it simply. - --path, the old mapping file, in window use \, in linux use /, and the default path is the running location. ![](http://i.imgur.com/y2LZRe9.gif) **4. Compress** Compress can specify the compression method for each file(Stored or Deflate). Generally, 1. blow 2.3 version, if the source file is larger than 1M, then is can not be compressed; 2, streaming media can not be compressed, such as .wav, .mpg. - --isactive, whether to use compress, you can set false to close it simply. - --path, you must use / separation, and it support *, ?, such as *.png, *.jpg, res/drawable-hdpi/welcome_?.png. The maximum confusion will be: 1. paths:*.png, *.jpg, *.jpeg, *.gif 2. resources.arsc ![](http://i.imgur.com/9lTPiPA.gif) **5. Sign** if you want to sign the apk, you should input following data, but if you want to use 7zip, you must fill them - --isactive, whether to use sign, you can set false to close it simply. - --path, the signature file path, in window use \, in linux use /, and the default path is the running location. - --storepass, storepass value. - --keypass, keypass value. - --alias, alias value. ![](http://i.imgur.com/21yO1jY.gif) Warning: if you use -signature mode。these setting in config.xml will be overlayed. ## FAQ 1. How to use compress flag If you use compess flag with *.png、*.gif、*.jpg,it will help you decrease the size of file `resources.arsc` NOTE: If your app support Android2.2 and below, the size of file `resources.arsc` should be below 1M. 2. keepmapping flag impact on the size of increasing package keepmapping will help to keep your coherence in different version 3. packages for different channel Repackage will make 7zip invalid,you should repackage all channel apk. 4. wanna get resource with `getIdentifier` You should add these resources to whitelist. NOTE: *You should add your icon to whitelist, because of some launchers' special implementation* 5. Use umeng or other sdk You should add umeng resource to whitelist ```xml ``` 6. Use fabric The Fabric Gradle plugin generates a unique identifier so that our backend can identify your builds. This identifier is used to deobfuscate crashes and distribute the correct versions of your beta test builds. The identifier will be added to your res/values and looks like: RANDOM_UUID The Fabric will get this value with exact name, so we add it to whitelist. ``` ".R.string.com.crashlytics.*" ``` ================================================ FILE: doc/how_to_work.zh-cn.md ================================================ ## 如何使用资源混淆工具 ## ### 使用命令行### **`tools_output`文件夹有使用命令行工具的简单例子,可以参考** 我们先看看它的help描述,最简单的使用方式是:java -jar andresguard.jar input.apk,此时会读取运行路径中的config.xml文件,并将结果输出到运行路径中的input(输入apk的名称)中。当然你也可以自己定义: -config, 指定具体config文件的路径; -out, 指定具体的输出路径;混淆的mapping会在输出文件夹中以resource_mapping_input(输入apk的名称).txt命名。 -signature, 指定签名信息,若在命令行设置会覆盖config.xml中的签名信息,顺序为签名文件路径、storepass、keypass、storealias。 -mapping, 指定旧的mapping文件,保证同一资源文件在不同版本混淆后的名称保持一致。若在命令行设置会覆盖config.xml中的信息。 -7zip, 指定7zip的路径,若已添加到环境变量不需要设置。应是全路径例如linux: /shwenzhang/tool/7za, Window需要加上.exe 结尾。 > window: > 对于window应下载命名行版本,若将7za指定到环境变量,即无须设置。地址:[http://sparanoid.com/lab/7z/download.html](http://sparanoid.com/lab/7z/download.html) > > linux:sudo apt-get install p7zip-full > > mac:brew install p7zip -zipalign, 指定zipalign的路径,若已添加到环境变量不需要设置。应是全路径例如linux: /shwenzhang/sdk/tools/zipalign, Window需要加上.exe结尾。 -repackage, 如果想要出渠道包等需求,我们可能希望利用7zip直接重打包安装包。 ![](http://i.imgur.com/FssA59a.jpg) **2.简单用法** java -jar andresguard.jar input.apk 若想指定配置文件或输出目录: java -jar andresguard.jar input.apk -config yourconfig.xml -out output_directory 若想指定签名信息或mapping信息: java -jar andresguard.jar input.apk -config yourconfig.xml -out output_directory -signature signature_file_path storepass_value keypass_value storealias_value -mapping mapping_file_path 若想指定7zip或zipalign的路径(若已设置环境变量,这两项不需要单独设置): java -jar andresguard.jar input.apk -7zip /shwenzhang/tool/7za -zipalign /shwenzhang/sdk/tools/zipalign 若想用7zip重打包安装包,同时也可指定output路径,指定7zip或zipalign的路径(此模式其他参数都不支持): java -jar andresguard.jar -repackage input.apk -out output_directory -7zip /shwenzhang/tool/7za -zipalign /shwenzhang/sdk/tools/zipalign ###使用资源混淆工具会得到什么 ## 正常来说,我们可得到以下output路径得到以下7个有用的文件:(需要把zipalign也加入环境变量) ![](http://i.imgur.com/UDtxKqO.png) 混淆过程中会输出log,主要是可看到耗费时间,以及相对输入apk减少的大小。 ![](http://i.imgur.com/ICDkJCH.png) ## 如何写配置文件 ## 配置文件中主要有五大项,即property,whitelist, keepmapping, compress,sign。 **1. Property项** Property主要设置一些通用属性: --sevenzip, 是否使用7z重新压缩签名后的apk包(这步一定要放在签名后,不然签名时会破坏效果),需要我们安装7z命令行,同时加入环境变量中,同时要求输入签名信息(不然不会使用)。 > Window:7z command line version, 即7za(http://www.7-zip.org/download.html) > > Linux: 可直接sudo apt-get install p7zip-full。 > > 注意:效果很好,推荐使用,并且在Linux(Mac的高富帅也可)上。 --metaname, 由于重打包时需要删除签名信息,考虑到这个文件名可能会被改变,所以使用者可手动输入签名信息对应的文件名。默认为META_INF。 --keeproot, 是否将res/drawable混淆成r/s ![](http://i.imgur.com/JfkZ09e.gif) **2. Whitelist项** Whitelist主要是用来设置白名单,由于我们代码中某些资源会通过getIdentifier(需要全局搜索所有用法并添加到白名单)或动态加载等方式,我们并不希望混淆这部分的资源ID: --isactive, 是否打开白名单功能; --path, 是白名单的项,格式为package_name.R.type.specname,由于一个resources.arsc中可能会有多个包,所以这里要求写全包名。同时支持*,?通配符,例如: com.tencent.mm.R.drawable.emoji_*、com.tencent.mm.R.drawable.emoji_?; 注意:1.不能写成com.tencent.mm.R.drawable.emoji.png,即带文件后缀名;2. *通配符代表.+,即a*,不能匹配到a; ![](http://i.imgur.com/VZ4fOa2.gif) **3. Keepmapping项** Keepmapping主要用来指定旧的mapping文件,为了保持一致性,我们支持输入旧的mapping文件,可保证同一资源文件在不同版本混淆后的名称保持一致。另一方面由于我们需要支持增量下载方式,如果每次改动都导致所有文件名都会更改,这会导致增量文件增大,但测试证明影响并不大(后面有测试数据)。 --isactive, 是否打开keepmapping模式; --path, 是旧mapping文件的位置,linux用/, window用 \; ![](http://i.imgur.com/y2LZRe9.gif) **4. Compress项** Compress主要用来指定文件重打包时是否压缩指定文件,默认我们重打包时是保持输入apk每个文件的压缩方式(即Stored或者Deflate)。一般来说,1、在2.3版本以下源文件大于1M不能压缩;2、流媒体不能压缩。对于.png、.jpg是可以压缩的,只是AssetManger读取时候的方式不同。 --isactive, 是否打开compress模式; --path, 是需要被压缩文件的相对路径(相对于apk最顶层的位置),这里明确一定要使用‘/’作为分隔符,同时支持通配符*,?,例如*.png(压缩所有.png文件),res/drawable/emjio_?.png,resouces.arsc(压缩 resources.arsc) 注意若想得到最大混淆: 1. 输入四项个path:*.png, *.jpg, *.jpeg, *.gif 2. 若你的resources.arsc原文件小于1M,可加入resourcs.arsc这一项!若不需要支持低版本,直接加入也可。 ![](http://i.imgur.com/9lTPiPA.gif) **5. Sign项** Sign主要是对处理后的文件重签名,需要我们输入签名文件位置,密码等信息。若想使用7z功能就一定要填入相关信息。 --isactive, 是否打开签名功能; --path, 是签名文件的位置,linux用/, window用 \; --storepass, 是storepass的数值; --keypass, 是keypass的数值; --alias, 是alias的数值; ![](http://i.imgur.com/21yO1jY.gif) 注意: 若出于保密不想写在config.xml,可用-signature命令行设置模式。config.xml中的签名信息会被命令行覆盖。 ## Android资源混淆工具需要注意的问题 ## 1. compress参数对混淆效果的影响 若指定compess 参数.png、.gif以及*.jpg,resources.arsc会大大减少安装包体积。若要支持Android2.2以及以下版本的设备,resources.arsc需保证压缩前小于1M。 2. 操作系统对7z的影响 实验证明,linux与mac的7z效果更好 3. keepmapping方式对增量包大小的影响 影响并不大,但使用keepmapping方式有利于保持所有版本混淆的一致性 4. 渠道包的问题(**建议通过修改zip摘要的方式生产渠道包**) 在出渠道包的时候,解压重压缩会破坏7zip的效果,通过repackage命令可用7zip重压缩。 5. 若想通过getIdentifier方式获得资源,需要放置白名单中。 部分手机桌面快捷图标的实现有问题,务必将程序桌面icon加入白名单。 6. 对于一些第三方sdk,例如友盟,可能需要将部分资源添加到白名单中。 ```xml ``` ## Androd资源混淆工具的耗时与效果 ## **1. 基本的耗时与效果** 以微信的5.4为例,使用组件中的resoureproguard.jar进行资源混淆,具体的性能数据如下: 其中时间指的是从最开始到该步骤完成的时间,而不是每步骤独立时间。 ![](http://i.imgur.com/8i62qbJ.jpg) **2. compres参数(下文有详细描述,是否压缩某些资源)对安装包大小的影响** 若指定compess 参数*.png、*.gif以及*.jpg,resources.arsc对安装包大小影响如下: ![](http://i.imgur.com/4kWgw6o.jpg) 但是resources.arsc如果原文件大于1M,压缩后是不能在系统2.3以下运行的。 **3. 操作系统对7z的影响** 由于7z过程中使用的是极限压缩模式,所以遍历次数会增多(7次),时间相对会比较长。假设不使用7z在单核的虚拟机中仅需10秒。 同时我们需要注意是由于文件系统不一致,在window上面使用7z生成的安装包会较大,微信在window以及linux下7z的效果如下: ![](http://i.imgur.com/t8SPz4Q.png) 所以最后出包请使用Linux(Mac亦可),具体原因应该与文件系统有关。 **4. keepmapping方式(下文有详细描述,是否保持旧的mapping)对增量包大小的影响** 我们一般使用bsdiff生成增量包,bsdiff差分的是二进制,利用LCS最长公共序列算法。假设分别使用正序与逆序混淆规则对微信5.4作资源混淆(即它们的混淆方式是完全相反的)。 ![](http://i.imgur.com/u7obKDt.png) 事实上,它们的差分是不需要371kb,因为有比较大的文件格式,共同标记部分。 现在我们做另外一个实验,首先对微信5.3.1作资源混淆得到安装包a,然后以keepmapping方式对微信5.4作资源混淆得到安装包b,最后以完全逆序的方式对微信5.4作资源混淆得到安装包c。 分别用安装包b、c对安装包a生成增量文件d,e。比较增量文件d、e的大小,分别如下: ![](http://i.imgur.com/MNY9AHr.png) 所以增量文件的大小并不是我们采用keepmapping方式的主要考虑因素,保持混淆的一致性,便于查找问题或是更加重要的考虑。 **5.安装包缩减的原因与影响因素** 总结,安装包大小减少的原因以下四个: ![](http://i.imgur.com/tkC2xr5.png) 相对的,可得到影响效果的因素有以下几个: ![](http://i.imgur.com/VEG9cP6.png) ================================================ FILE: doc/white_list.md ================================================ ### Umeng sdk ``` "R.anim.umeng*", "R.string.umeng*", "R.string.UM*", "R.string.tb_*", "R.layout.umeng*", "R.layout.socialize_*", "R.layout.*messager*", "R.layout.tb_*", "R.color.umeng*", "R.color.tb_*", "R.style.*UM*", "R.style.umeng*", "R.drawable.umeng*", "R.drawable.tb_*", "R.drawable.sina*", "R.drawable.qq_*", "R.drawable.tb_*", "R.id.umeng*", "R.id.*messager*", "R.id.progress_bar_parent", "R.id.socialize_*", "R.id.webView" ``` ### google-services ``` "R.string.google_app_id", "R.string.gcm_defaultSenderId", "R.string.default_web_client_id", "R.string.ga_trackingId", "R.string.firebase_database_url", "R.string.google_api_key", "R.string.google_crash_reporting_api_key" ``` ### getui(个推) ``` "R.drawable.push", "R.drawable.push_small", "R.layout.getui_notification" ``` ### JPush(极光推送) ``` "R.drawable.jpush_notification_icon" ``` ### GrowingIO ``` "R.string.growingio_project_id", "R.string.growingio_url_scheme", "R.string.growingio_channel" ``` ### Firebase #### [firStore](https://firebase.google.cn/docs/firestore/) ``` R.string.project_id ``` ### Huawei push ``` "R.string.hms_*", "R.string.connect_server_fail_prompt_toast", "R.string.getting_message_fail_prompt_toast", "R.string.no_available_network_prompt_toast", "R.string.third_app_*", "R.string.upsdk_*", "R.style.upsdkDlDialog", "R.style.AppTheme", "R.style.AppBaseTheme", "R.dimen.upsdk_dialog_*", "R.color.upsdk_*", "R.layout.upsdk_*", "R.drawable.upsdk_*", "R.drawable.hms_*", "R.layout.hms_*", "R.id.hms_*" ``` ### Firebase Crashlytics ``` "R.bool.com.crashlytics.useFirebaseAppId", "R.string.com.crashlytics.useFirebaseAppId", "R.string.google_app_id", "R.bool.com.crashlytics.CollectDeviceIdentifiers", "R.string.com.crashlytics.CollectDeviceIdentifiers", "R.bool.com.crashlytics.CollectUserIdentifiers", "R.string.com.crashlytics.CollectUserIdentifiers", "R.string.com.crashlytics.ApiEndpoint", "R.string.io.fabric.android.build_id", "R.string.com.crashlytics.android.build_id", "R.bool.com.crashlytics.RequireBuildId", "R.string.com.crashlytics.RequireBuildId", "R.bool.com.crashlytics.CollectCustomLogs", "R.string.com.crashlytics.CollectCustomLogs", "R.bool.com.crashlytics.Trace", "R.string.com.crashlytics.Trace", "R.string.com.crashlytics.CollectCustomKeys" ``` ### shareSDK ``` "R.id.ssdk*", "R.string.mobcommon*", "R.string.ssdk*", "R.string.mobdemo*", "R.drawable.mobcommon*", "R.drawable.ssdk*", "R.layout.mob*", "R.style.mobcommon*", ``` ### 穿山甲广告SDK ``` "R.string.tt_*", "R.integer.tt_*", "R.layout.tt_*", "R.drawable.tt_*", "R.style.tt_*", "R.dimen.tt_*", "R.anim.tt_*", "R.color.tt_*", "R.id.tt_*" ``` ================================================ FILE: gradle/gradle-mvn-push.gradle ================================================ apply plugin: 'maven' apply plugin: 'com.jfrog.bintray' def getBintrayUser() { return hasProperty('BINTRAY_USER') ? BINTRAY_USER : readPropertyFromLocalProperties('BINTRAY_USER') } def getBintrayKey() { return hasProperty('BINTRAY_APIKEY') ? BINTRAY_APIKEY : readPropertyFromLocalProperties('BINTRAY_APIKEY') } def readPropertyFromLocalProperties(String key) { Properties properties = new Properties() try { properties.load(project.rootProject.file('local.properties').newDataInputStream()) } catch (Exception ignore) { } return properties.getProperty(key) } bintray { user = getBintrayUser() key = getBintrayKey() configurations = ['archives'] publications = ['ResguardPub'] pkg { repo = 'maven' userOrg = BINTRAY_ORGANIZATION name = "${GROUP}:${POM_ARTIFACT_ID}" desc = POM_DESCRIPTION licenses = BINTRAY_LICENCE vcsUrl = POM_SCM_URL websiteUrl = POM_URL issueTrackerUrl = POM_ISSUE_URL publicDownloadNumbers = true publish = true dryRun = false } } task buildAndPublishRepo(dependsOn: ['build', 'uploadArchives']) { doLast { println "*published to repo: ${project.group}:${project.name}:${project.version}" } } ================================================ FILE: gradle/java-artifacts.gradle ================================================ apply plugin: 'maven-publish' sourceCompatibility = rootProject.ext.javaVersion targetCompatibility = rootProject.ext.javaVersion task sourcesJar(type: Jar) { from sourceSets.main.java.srcDirs classifier = 'sources' } task javadocJar(type: Jar, dependsOn: javadoc) { classifier = 'javadoc' from javadoc.destinationDir } artifacts { archives javadocJar archives sourcesJar } publishing { publications { ResguardPub(MavenPublication) { from components.java groupId = group artifactId = POM_ARTIFACT_ID version = version } } } task buildAndPublishLocalMaven(dependsOn: ['build', 'publishResguardPublicationToMavenLocal']) {} ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Jan 01 00:19:05 CST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true ANDRESGUARD_VESSION=1.2.21 ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: package.json ================================================ { "name": "AndResGuard", "version": "1.0.0", "description": "[![Join the chat at https://gitter.im/shwenzhang/AndResGuard](https://badges.gitter.im/shwenzhang/AndResGuard.svg)](https://gitter.im/shwenzhang/AndResGuard?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/shwenzhang/AndResGuard.svg?branch=master)](https://travis-ci.org/shwenzhang/AndResGuard) [![Jcenter Status](https://api.bintray.com/packages/simsun/maven/AndResGuard-gradle-plugin/images/download.svg)](https://bintray.com/simsun/maven/AndResGuard-gradle-plugin) [![license](http://img.shields.io/badge/license-apache_2.0-red.svg?style=flat)](https://raw.githubusercontent.com/shwenzhang/AndResGuard/master/LICENSE)", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/shwenzhang/AndResGuard.git" }, "keywords": [], "author": "", "license": "ISC", "bugs": { "url": "https://github.com/shwenzhang/AndResGuard/issues" }, "homepage": "https://github.com/shwenzhang/AndResGuard#readme", "devDependencies": { "cz-conventional-changelog": "^1.1.6" }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } } } ================================================ FILE: settings.gradle ================================================ include ':AndResGuard-core', ':AndResGuard-gradle-plugin', ':AndResGuard-cli' include 'SevenZip' ================================================ FILE: tool_output/build_apk.bat ================================================ set jdkpath=D:\Program Files\Java\jdk1.7.0_79\bin\java.exe set storepath=release.keystore set storepass=testres set keypass=testres set alias=testres set zipalign=D:\soft\dev\android\sdk\build-tools\23.0.2\zipalign.exe "%jdkpath%" -jar AndResGuard-cli-1.2.15.jar input.apk -config config.xml -out outapk -signature "%storepath%" "%storepass%" "%keypass%" "%alias%" -zipalign "%zipalign%" pause ================================================ FILE: tool_output/build_apk.sh ================================================ #!/usr/bin/env bash java -jar AndResGuard-cli-1.2.15.jar input.apk -config config.xml -out outapk -signatureType v2 -signature release.keystore testres testres testres ================================================ FILE: tool_output/config.xml ================================================