├── .gitignore ├── .idea ├── .gitignore ├── artifacts │ ├── composeApp_desktop.xml │ ├── nmmp_jvm.xml │ └── nmmp_jvm_1_0_SNAPSHOT.xml ├── compiler.xml ├── gradle.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── ART └── Screenshot.png ├── README.md ├── build.gradle.kts ├── composeApp ├── build.gradle.kts └── src │ └── desktopMain │ ├── kotlin │ ├── Main.kt │ ├── composition │ │ ├── ButtonDefault.kt │ │ ├── DirectoryPathInputField.kt │ │ └── FilePathInputField.kt │ ├── data │ │ └── AppPrefs.kt │ ├── task │ │ ├── AabSignTask.kt │ │ ├── AabVmpTask.kt │ │ ├── AarVmpTask.kt │ │ ├── ApkSignTask.kt │ │ └── ApkVmpTask.kt │ └── utils │ │ └── FilePicker.kt │ └── resources │ ├── apksigner.jar │ └── ic_folder_open.svg ├── desktop-rules.pro ├── gradle.properties ├── gradle ├── libs.version.toml ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── library ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── java │ └── com │ │ └── nmmedit │ │ └── apkprotect │ │ ├── ApkFolders.java │ │ ├── ApkProtect.java │ │ ├── BuildNativeLib.java │ │ ├── aab │ │ ├── AabFolders.java │ │ ├── AabProtect.java │ │ └── proto │ │ │ └── ProtoUtils.java │ │ ├── aar │ │ ├── AarFolders.java │ │ ├── AarProtect.java │ │ └── asm │ │ │ ├── AsmMethod.java │ │ │ ├── AsmUtils.java │ │ │ ├── InjectStaticBlockVisitor.java │ │ │ └── MethodToNativeVisitor.java │ │ ├── data │ │ ├── Prefs.kt │ │ └── Storage.kt │ │ ├── deobfus │ │ ├── MappingProcessor.java │ │ └── MappingReader.java │ │ ├── dex2c │ │ ├── Dex2c.java │ │ ├── DexConfig.java │ │ ├── GlobalDexConfig.java │ │ ├── converter │ │ │ ├── ClassAnalyzer.java │ │ │ ├── JniCodeGenerator.java │ │ │ ├── MyMethodUtil.java │ │ │ ├── References.java │ │ │ ├── ResolverCodeGenerator.java │ │ │ ├── instructionrewriter │ │ │ │ ├── InstructionRewriter.java │ │ │ │ ├── NoneInstructionRewriter.java │ │ │ │ └── RandomInstructionRewriter.java │ │ │ ├── structs │ │ │ │ ├── EmptyAnnotationMethodParameter.java │ │ │ │ ├── EmptyConstructorMethod.java │ │ │ │ ├── LoadLibStaticBlockMethod.java │ │ │ │ ├── MethodConverter.java │ │ │ │ ├── MyClassDef.java │ │ │ │ ├── RegisterNativesCallerClassDef.java │ │ │ │ └── RegisterNativesUtilClassDef.java │ │ │ └── testbuild │ │ │ │ ├── ClassMethodImplCollection.java │ │ │ │ └── JniTemp.java │ │ └── filters │ │ │ ├── BasicKeepConfig.java │ │ │ ├── ClassAndMethodFilter.java │ │ │ ├── ProguardMappingConfig.java │ │ │ ├── SimpleConvertConfig.java │ │ │ └── SimpleRules.java │ │ ├── log │ │ └── VmpLogger.kt │ │ ├── sign │ │ ├── ApkVerifyCodeGenerator.java │ │ └── PublicKeyUtils.java │ │ └── util │ │ ├── ApkUtils.java │ │ ├── CmakeUtils.java │ │ ├── FileHelper.kt │ │ ├── FileUtils.java │ │ ├── ModifiedUtf8.java │ │ ├── OsDetector.kt │ │ ├── Pair.kt │ │ └── ZipHelper.kt │ ├── proto │ ├── Configuration.proto │ ├── Resources.proto │ ├── apex_manifest.proto │ ├── app_dependencies.proto │ ├── app_integrity_config.proto │ ├── build_stamp.proto │ ├── code_transparency.proto │ ├── commands.proto │ ├── config.proto │ ├── device_targeting_config.proto │ ├── devices.proto │ ├── errors.proto │ ├── files.proto │ ├── rotation_config.proto │ ├── runtime_enabled_sdk_config.proto │ ├── sdk_bundle_config.proto │ ├── sdk_metadata.proto │ ├── sdk_modules_config.proto │ ├── sizes.proto │ └── targeting.proto │ └── resources │ └── vmsrc.zip └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store 43 | 44 | /wix311 45 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/artifacts/composeApp_desktop.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/composeApp/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/nmmp_jvm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/build/libs 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/artifacts/nmmp_jvm_1_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ART/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timscriptov/nmmp/ceb3417f756a4193dfe2ae24da27cf2a04ee1470/ART/Screenshot.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/TimScriptov/nmmp.svg)](https://jitpack.io/#TimScriptov/nmmp) 2 | 3 | ## Original REPO: 4 | https://github.com/maoabc/nmmp 5 | 6 | # Screenshots 7 | ![Main](/ART/Screenshot.png) 8 | 9 | ## Add it in your root build.gradle at the end of repositories: 10 | ```groovy 11 | allprojects { 12 | repositories { 13 | maven { url 'https://jitpack.io' } 14 | } 15 | } 16 | ``` 17 | 18 | ## Add the dependency: 19 | ```groovy 20 | dependencies { 21 | implementation("com.github.TimScriptov:nmmp:Tag") 22 | implementation("com.github.TimScriptov:preferences:Tag") 23 | } 24 | ``` 25 | 26 | ## Init preferences: 27 | ```kotlin 28 | Preferences(File("path"), "name.json").init() 29 | ``` 30 | 31 | ## Environment path: 32 | ```kotlin 33 | Prefs.setSdkPath("path") // ANDROID_SDK_HOME 34 | Prefs.setCmakePath("path") // CMAKE_PATH 35 | Prefs.setNdkPath("path") // ANDROID_NDK_HOME 36 | ``` 37 | 38 | ## Change lib name and class name: 39 | ```kotlin 40 | Prefs.setRegisterNativesClassName("com/nmmedit/protect/NativeUtil") 41 | Prefs.setVmName("nmmvm") 42 | Prefs.setNmmpName("nmmp") 43 | ``` 44 | 45 | ## Protect APK: 46 | ```kotlin 47 | val input = File("input.apk") 48 | val output = File("output.apk") 49 | val rules = File("rules.txt") 50 | val simpleRules = SimpleRules().apply { 51 | parse(InputStreamReader(FileInputStream(rules), StandardCharsets.UTF_8)) 52 | } 53 | val filterConfig = SimpleConvertConfig(BasicKeepConfig(), simpleRules) 54 | ApkProtect.Builder(ApkFolders(input, output)).apply { 55 | setInstructionRewriter(RandomInstructionRewriter()) 56 | setFilter(filterConfig) 57 | setLogger(null) 58 | setClassAnalyzer(ClassAnalyzer()) 59 | }.build().run() 60 | ``` 61 | 62 | ## Protect AAR: 63 | ```kotlin 64 | val input = File("input.aar") 65 | val output = File("output.aar") 66 | val rules = File("rules.txt") 67 | val simpleRules = SimpleRules().apply { 68 | parse(InputStreamReader(FileInputStream(rules), StandardCharsets.UTF_8)) 69 | } 70 | val filterConfig = SimpleConvertConfig(BasicKeepConfig(), simpleRules) 71 | AarProtect.Builder(AarFolders(input, output)).apply { 72 | setInstructionRewriter(RandomInstructionRewriter()) 73 | setFilter(filterConfig) 74 | setLogger(null) 75 | setClassAnalyzer(ClassAnalyzer()) 76 | }.build().run() 77 | ``` 78 | 79 | ## Protect AAB: 80 | ```kotlin 81 | val input = File("input.aab") 82 | val output = File("output.aab") 83 | val rules = File("rules.txt") 84 | val simpleRules = SimpleRules().apply { 85 | parse(InputStreamReader(FileInputStream(rules), StandardCharsets.UTF_8)) 86 | } 87 | val filterConfig = SimpleConvertConfig(BasicKeepConfig(), simpleRules) 88 | AabProtect.Builder(AabFolders(input, output)).apply { 89 | setInstructionRewriter(RandomInstructionRewriter()) 90 | setFilter(filterConfig) 91 | setLogger(null) 92 | setClassAnalyzer(ClassAnalyzer()) 93 | }.build().run() 94 | ``` 95 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // this is necessary to avoid the plugins to be loaded multiple times 3 | // in each subproject's classloader 4 | alias(libs.plugins.jetbrainsCompose) apply false 5 | alias(libs.plugins.kotlinMultiplatform) apply false 6 | } 7 | -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.ExperimentalComposeLibrary 2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 3 | 4 | plugins { 5 | alias(libs.plugins.kotlinMultiplatform) 6 | alias(libs.plugins.jetbrainsCompose) 7 | alias(libs.plugins.kotlinx.serialization) 8 | } 9 | 10 | kotlin { 11 | jvm("desktop") { 12 | jvmToolchain(17) 13 | withJava() 14 | } 15 | 16 | sourceSets { 17 | val desktopMain by getting 18 | 19 | commonMain.dependencies { 20 | implementation(project(":library")) 21 | 22 | implementation(compose.runtime) 23 | implementation(compose.foundation) 24 | implementation(compose.material3) 25 | implementation(compose.ui) 26 | @OptIn(ExperimentalComposeLibrary::class) 27 | implementation(compose.components.resources) 28 | 29 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") 30 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.2") 31 | 32 | implementation("com.github.timscriptov:preferences:1.0.4") 33 | 34 | implementation("org.slf4j:slf4j-simple:1.6.1") 35 | } 36 | desktopMain.dependencies { 37 | implementation(compose.desktop.currentOs) 38 | } 39 | } 40 | } 41 | 42 | compose.desktop { 43 | application { 44 | mainClass = "MainKt" 45 | nativeDistributions { 46 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 47 | packageName = "nmmp" 48 | packageVersion = "1.3.0" 49 | description = "Android APK Protector" 50 | copyright = "© 2023 timscriptov." 51 | vendor = "timscriptov" 52 | } 53 | buildTypes.release.proguard { 54 | version.set("7.3.2") 55 | configurationFiles.from("desktop-rules.pro") 56 | isEnabled.set(true) 57 | obfuscate.set(true) 58 | } 59 | jvmArgs("-Djdk.util.zip.disableZip64ExtraFieldValidation=true") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/composition/ButtonDefault.kt: -------------------------------------------------------------------------------- 1 | package composition 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.PaddingValues 5 | import androidx.compose.material3.Button 6 | import androidx.compose.material3.ButtonDefaults 7 | import androidx.compose.material3.ShapeDefaults 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.text.TextStyle 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.unit.dp 15 | import androidx.compose.ui.unit.sp 16 | 17 | @Composable 18 | fun ButtonDefault( 19 | text: String, 20 | onClick: () -> Unit, 21 | enabled: Boolean = true, 22 | modifier: Modifier = Modifier, 23 | ) { 24 | Button( 25 | modifier = modifier, 26 | enabled = enabled, 27 | border = BorderStroke(1.dp, Color.Gray), 28 | colors = ButtonDefaults.filledTonalButtonColors(), 29 | shape = ShapeDefaults.Small, 30 | onClick = onClick, 31 | contentPadding = PaddingValues( 32 | horizontal = 10.dp, 33 | vertical = 6.dp 34 | ) 35 | ) { 36 | Text( 37 | text = text, 38 | style = TextStyle( 39 | fontSize = 16.sp, 40 | fontWeight = FontWeight(500) 41 | ) 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/composition/DirectoryPathInputField.kt: -------------------------------------------------------------------------------- 1 | package composition 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.res.painterResource 8 | import androidx.compose.ui.unit.dp 9 | import utils.FilePicker 10 | 11 | @OptIn(ExperimentalMaterial3Api::class) 12 | @Composable 13 | fun DirectoryPathInputField( 14 | modifier: Modifier, 15 | hintText: String, 16 | selectText: String, 17 | inputValue: String, 18 | onValueChange: (String) -> Unit 19 | ) { 20 | OutlinedTextField( 21 | modifier = modifier, 22 | value = inputValue, 23 | label = { Text(hintText) }, 24 | onValueChange = onValueChange, 25 | shape = ShapeDefaults.Small, 26 | trailingIcon = { 27 | IconButton( 28 | modifier = Modifier.padding(4.dp), 29 | onClick = { 30 | FilePicker.chooseDirectory( 31 | selectText, 32 | FilePicker.getParentDirectory(inputValue) 33 | )?.let { onValueChange(it) } 34 | } 35 | ) { 36 | Icon(painter = painterResource("ic_folder_open.svg"), contentDescription = "Select directory") 37 | } 38 | } 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/composition/FilePathInputField.kt: -------------------------------------------------------------------------------- 1 | package composition 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.res.painterResource 8 | import androidx.compose.ui.unit.dp 9 | import utils.FilePicker 10 | 11 | @Composable 12 | fun FilePathInputField( 13 | modifier: Modifier, 14 | inputHint: String, 15 | selectionDescription: String, 16 | inputValue: String, 17 | onValueChange: (String) -> Unit 18 | ) { 19 | OutlinedTextField( 20 | modifier = modifier, 21 | value = inputValue, 22 | label = { Text(inputHint) }, 23 | onValueChange = onValueChange, 24 | shape = ShapeDefaults.Small, 25 | trailingIcon = { 26 | IconButton( 27 | modifier = Modifier.padding(4.dp), 28 | onClick = { 29 | FilePicker.chooseFile( 30 | selectionDescription, 31 | FilePicker.getParentDirectory(inputValue) 32 | )?.let { onValueChange(it) } 33 | } 34 | ) { 35 | Icon(painter = painterResource("ic_folder_open.svg"), contentDescription = "Select file") 36 | } 37 | } 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/data/AppPrefs.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import com.mcal.preferences.PreferencesManager 4 | import com.nmmedit.apkprotect.data.Prefs 5 | import com.nmmedit.apkprotect.data.Storage 6 | 7 | object AppPrefs : PreferencesManager(Storage.binDir, "nmmp_preferences.json") { 8 | /** 9 | * ApkSigner 10 | */ 11 | @JvmStatic 12 | fun keystorePath(): String { 13 | return Prefs.getString("keystore_path", "") 14 | } 15 | 16 | @JvmStatic 17 | fun setKeystorePath(name: String) { 18 | Prefs.putString("keystore_path", name) 19 | } 20 | 21 | @JvmStatic 22 | fun keystorePass(): String { 23 | return Prefs.getString("keystore_pass", "") 24 | } 25 | 26 | @JvmStatic 27 | fun setKeystorePass(name: String) { 28 | Prefs.putString("keystore_pass", name) 29 | } 30 | 31 | @JvmStatic 32 | fun keystoreAlias(): String { 33 | return Prefs.getString("keystore_alias", "") 34 | } 35 | 36 | @JvmStatic 37 | fun setKeystoreAlias(name: String) { 38 | Prefs.putString("keystore_alias", name) 39 | } 40 | 41 | @JvmStatic 42 | fun keystoreAliasPass(): String { 43 | return Prefs.getString("keystore_alias_pass", "") 44 | } 45 | 46 | @JvmStatic 47 | fun setKeystoreAliasPass(name: String) { 48 | Prefs.putString("keystore_alias_pass", name) 49 | } 50 | 51 | /** 52 | * Config 53 | */ 54 | @JvmStatic 55 | fun rulesPath(): String { 56 | return Prefs.getString("rules_path", "") 57 | } 58 | 59 | @JvmStatic 60 | fun setRulesPath(name: String) { 61 | Prefs.putString("rules_path", name) 62 | } 63 | 64 | @JvmStatic 65 | fun mappingPath(): String { 66 | return Prefs.getString("mapping_path", "") 67 | } 68 | 69 | @JvmStatic 70 | fun setMappingPath(name: String) { 71 | Prefs.putString("mapping_path", name) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/task/AabSignTask.kt: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import com.nmmedit.apkprotect.log.VmpLogger 4 | import java.io.BufferedReader 5 | import java.io.IOException 6 | import java.io.InputStream 7 | import java.io.InputStreamReader 8 | import java.nio.file.Files 9 | import java.nio.file.Paths 10 | 11 | class AabSignTask( 12 | private val output: String, 13 | private val keystorePath: String, 14 | private val keystorePassword: String, 15 | private val keystoreAlias: String, 16 | private val keystoreAliasPassword: String, 17 | private val logs: MutableList, 18 | ) : VmpLogger { 19 | fun start() { 20 | if (keystorePath.isEmpty()) { 21 | info("No keystore, skip sign.") 22 | return 23 | } 24 | 25 | if (!Files.exists(Paths.get(keystorePath))) { 26 | info("Keystore $keystorePath does not exist, skip sign.") 27 | return 28 | } 29 | 30 | 31 | try { 32 | execCmd( 33 | listOf( 34 | "jarsigner", 35 | "-sigalg", 36 | "SHA256withRSA", 37 | "-digestalg", 38 | "SHA-256", 39 | "-keystore", 40 | keystorePath, 41 | "-storepass", 42 | keystorePassword, 43 | "-keypass", 44 | keystoreAliasPassword, 45 | output, 46 | keystoreAlias, 47 | "-tsa", 48 | "http://sha256timestamp.ws.symantec.com/sha256/timestamp" 49 | ), this 50 | ) 51 | } catch (e: IOException) { 52 | e.printStackTrace() 53 | } 54 | } 55 | 56 | @Throws(IOException::class) 57 | private fun execCmd(cmds: List, logger: VmpLogger?) { 58 | if (logger != null) { 59 | logger.info(cmds.toString()) 60 | } else { 61 | println(cmds) 62 | } 63 | val builder = ProcessBuilder() 64 | .command(cmds) 65 | 66 | val process = builder.start() 67 | 68 | printOutput(process.inputStream, logger) 69 | printOutput(process.errorStream, logger) 70 | 71 | try { 72 | val exitStatus = process.waitFor() 73 | if (exitStatus != 0) { 74 | if (logger != null) { 75 | logger.error(String.format("Cmd '%s' exec failed", cmds)) 76 | } else { 77 | throw IOException(String.format("Cmd '%s' exec failed", cmds)) 78 | } 79 | } 80 | } catch (e: InterruptedException) { 81 | e.printStackTrace() 82 | } 83 | } 84 | 85 | @Throws(IOException::class) 86 | private fun printOutput(inputStream: InputStream, logger: VmpLogger?) { 87 | val reader = BufferedReader(InputStreamReader(inputStream)) 88 | var line: String? 89 | while ((reader.readLine().also { line = it }) != null) { 90 | if (logger != null) { 91 | logger.info(line) 92 | } else { 93 | println(line) 94 | } 95 | } 96 | } 97 | 98 | override fun info(msg: String?) { 99 | if (!msg.isNullOrEmpty()) { 100 | logs.add("I: $msg") 101 | } 102 | } 103 | 104 | override fun error(msg: String?) { 105 | if (!msg.isNullOrEmpty()) { 106 | logs.add("E: $msg") 107 | } 108 | } 109 | 110 | override fun warning(msg: String?) { 111 | if (!msg.isNullOrEmpty()) { 112 | logs.add("W: $msg") 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/task/AabVmpTask.kt: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import com.nmmedit.apkprotect.aab.AabFolders 4 | import com.nmmedit.apkprotect.aab.AabProtect 5 | import com.nmmedit.apkprotect.deobfus.MappingReader 6 | import com.nmmedit.apkprotect.dex2c.converter.ClassAnalyzer 7 | import com.nmmedit.apkprotect.dex2c.converter.instructionrewriter.RandomInstructionRewriter 8 | import com.nmmedit.apkprotect.dex2c.filters.BasicKeepConfig 9 | import com.nmmedit.apkprotect.dex2c.filters.ProguardMappingConfig 10 | import com.nmmedit.apkprotect.dex2c.filters.SimpleConvertConfig 11 | import com.nmmedit.apkprotect.dex2c.filters.SimpleRules 12 | import com.nmmedit.apkprotect.log.VmpLogger 13 | import java.io.File 14 | import java.io.FileInputStream 15 | import java.io.InputStreamReader 16 | import java.nio.charset.StandardCharsets 17 | 18 | class AabVmpTask( 19 | private val input: String, 20 | private val output: String, 21 | private val rules: String, 22 | private val mapping: String, 23 | private val logs: MutableList, 24 | ) : VmpLogger { 25 | fun start() { 26 | val simpleRules = SimpleRules().apply { 27 | parse(InputStreamReader(FileInputStream(rules), StandardCharsets.UTF_8)) 28 | } 29 | val filterConfig = if (mapping.isNotEmpty()) { 30 | ProguardMappingConfig(BasicKeepConfig(), MappingReader(File(mapping)), simpleRules) 31 | } else { 32 | SimpleConvertConfig(BasicKeepConfig(), simpleRules) 33 | } 34 | AabProtect.Builder(AabFolders(File(input), File(output))).apply { 35 | setInstructionRewriter(RandomInstructionRewriter()) 36 | setFilter(filterConfig) 37 | setLogger(this@AabVmpTask) 38 | setClassAnalyzer(ClassAnalyzer()) 39 | }.build().run() 40 | } 41 | 42 | override fun info(msg: String?) { 43 | if (!msg.isNullOrEmpty()) { 44 | logs.add("I: $msg") 45 | } 46 | } 47 | 48 | override fun error(msg: String?) { 49 | if (!msg.isNullOrEmpty()) { 50 | logs.add("E: $msg") 51 | } 52 | } 53 | 54 | override fun warning(msg: String?) { 55 | if (!msg.isNullOrEmpty()) { 56 | logs.add("W: $msg") 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/task/AarVmpTask.kt: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import com.nmmedit.apkprotect.aar.AarFolders 4 | import com.nmmedit.apkprotect.aar.AarProtect 5 | import com.nmmedit.apkprotect.deobfus.MappingReader 6 | import com.nmmedit.apkprotect.dex2c.converter.ClassAnalyzer 7 | import com.nmmedit.apkprotect.dex2c.converter.instructionrewriter.RandomInstructionRewriter 8 | import com.nmmedit.apkprotect.dex2c.filters.BasicKeepConfig 9 | import com.nmmedit.apkprotect.dex2c.filters.ProguardMappingConfig 10 | import com.nmmedit.apkprotect.dex2c.filters.SimpleConvertConfig 11 | import com.nmmedit.apkprotect.dex2c.filters.SimpleRules 12 | import com.nmmedit.apkprotect.log.VmpLogger 13 | import java.io.File 14 | import java.io.FileInputStream 15 | import java.io.InputStreamReader 16 | import java.nio.charset.StandardCharsets 17 | 18 | class AarVmpTask( 19 | private val input: String, 20 | private val output: String, 21 | private val rules: String, 22 | private val mapping: String, 23 | private val logs: MutableList, 24 | ) : VmpLogger { 25 | fun start() { 26 | val simpleRules = SimpleRules().apply { 27 | parse(InputStreamReader(FileInputStream(rules), StandardCharsets.UTF_8)) 28 | } 29 | val filterConfig = if (mapping.isNotEmpty()) { 30 | ProguardMappingConfig(BasicKeepConfig(), MappingReader(File(mapping)), simpleRules) 31 | } else { 32 | SimpleConvertConfig(BasicKeepConfig(), simpleRules) 33 | } 34 | AarProtect.Builder(AarFolders(File(input), File(output))).apply { 35 | setInstructionRewriter(RandomInstructionRewriter()) 36 | setFilter(filterConfig) 37 | setLogger(this@AarVmpTask) 38 | setClassAnalyzer(ClassAnalyzer()) 39 | }.build().run() 40 | } 41 | 42 | override fun info(msg: String?) { 43 | if (!msg.isNullOrEmpty()) { 44 | logs.add("I: $msg") 45 | } 46 | } 47 | 48 | override fun error(msg: String?) { 49 | if (!msg.isNullOrEmpty()) { 50 | logs.add("E: $msg") 51 | } 52 | } 53 | 54 | override fun warning(msg: String?) { 55 | if (!msg.isNullOrEmpty()) { 56 | logs.add("W: $msg") 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/task/ApkSignTask.kt: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import com.nmmedit.apkprotect.log.VmpLogger 4 | import com.nmmedit.apkprotect.util.FileHelper.copyStream 5 | import com.nmmedit.apkprotect.util.FileUtils 6 | import java.io.* 7 | import java.nio.file.Files 8 | import java.nio.file.Paths 9 | 10 | class ApkSignTask( 11 | private val output: String, 12 | private val keystorePath: String, 13 | private val keystorePassword: String, 14 | private val keystoreAlias: String, 15 | private val keystoreAliasPassword: String, 16 | private val logs: MutableList, 17 | ) : VmpLogger { 18 | fun start() { 19 | if (keystorePath.isEmpty()) { 20 | info("No keystore, skip sign.") 21 | return 22 | } 23 | 24 | if (!Files.exists(Paths.get(keystorePath))) { 25 | info("Keystore $keystorePath does not exist, skip sign.") 26 | return 27 | } 28 | 29 | val apksignerFile = File(FileUtils.getHomePath(), "tools/apksigner.jar") 30 | if (!apksignerFile.exists()) { 31 | apksignerFile.parentFile.mkdirs() 32 | ApkSignTask::class.java.getResourceAsStream("/apksigner.jar").use { inputStream -> 33 | FileOutputStream(apksignerFile).use { outputStream -> 34 | inputStream?.let { copyStream(it, outputStream) } 35 | } 36 | } 37 | } 38 | 39 | try { 40 | execCmd( 41 | listOf( 42 | "java", 43 | "-jar", 44 | apksignerFile.absolutePath, 45 | "sign", 46 | "--in", 47 | output, 48 | "--out", 49 | output, 50 | "--ks", 51 | keystorePath, 52 | "--ks-pass", 53 | "pass:$keystorePassword", 54 | "--ks-key-alias", 55 | keystoreAlias, 56 | "--key-pass", 57 | "pass:$keystoreAliasPassword", 58 | ), this 59 | ) 60 | } catch (e: IOException) { 61 | e.printStackTrace() 62 | } 63 | } 64 | 65 | @Throws(IOException::class) 66 | private fun execCmd(cmds: List, logger: VmpLogger?) { 67 | if (logger != null) { 68 | logger.info(cmds.toString()) 69 | } else { 70 | println(cmds) 71 | } 72 | val builder = ProcessBuilder() 73 | .command(cmds) 74 | 75 | val process = builder.start() 76 | 77 | printOutput(process.inputStream, logger) 78 | printOutput(process.errorStream, logger) 79 | 80 | try { 81 | val exitStatus = process.waitFor() 82 | if (exitStatus != 0) { 83 | if (logger != null) { 84 | logger.error(String.format("Cmd '%s' exec failed", cmds)) 85 | } else { 86 | throw IOException(String.format("Cmd '%s' exec failed", cmds)) 87 | } 88 | } 89 | } catch (e: InterruptedException) { 90 | e.printStackTrace() 91 | } 92 | } 93 | 94 | @Throws(IOException::class) 95 | private fun printOutput(inputStream: InputStream, logger: VmpLogger?) { 96 | val reader = BufferedReader(InputStreamReader(inputStream)) 97 | var line: String? 98 | while ((reader.readLine().also { line = it }) != null) { 99 | if (logger != null) { 100 | logger.info(line) 101 | } else { 102 | println(line) 103 | } 104 | } 105 | } 106 | 107 | override fun info(msg: String?) { 108 | if (!msg.isNullOrEmpty()) { 109 | logs.add("I: $msg") 110 | } 111 | } 112 | 113 | override fun error(msg: String?) { 114 | if (!msg.isNullOrEmpty()) { 115 | logs.add("E: $msg") 116 | } 117 | } 118 | 119 | override fun warning(msg: String?) { 120 | if (!msg.isNullOrEmpty()) { 121 | logs.add("W: $msg") 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/task/ApkVmpTask.kt: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import com.nmmedit.apkprotect.ApkFolders 4 | import com.nmmedit.apkprotect.ApkProtect 5 | import com.nmmedit.apkprotect.deobfus.MappingReader 6 | import com.nmmedit.apkprotect.dex2c.converter.ClassAnalyzer 7 | import com.nmmedit.apkprotect.dex2c.converter.instructionrewriter.RandomInstructionRewriter 8 | import com.nmmedit.apkprotect.dex2c.filters.BasicKeepConfig 9 | import com.nmmedit.apkprotect.dex2c.filters.ProguardMappingConfig 10 | import com.nmmedit.apkprotect.dex2c.filters.SimpleConvertConfig 11 | import com.nmmedit.apkprotect.dex2c.filters.SimpleRules 12 | import com.nmmedit.apkprotect.log.VmpLogger 13 | import java.io.File 14 | import java.io.FileInputStream 15 | import java.io.InputStreamReader 16 | import java.nio.charset.StandardCharsets 17 | 18 | class ApkVmpTask( 19 | private val input: String, 20 | private val output: String, 21 | private val rules: String, 22 | private val mapping: String, 23 | private val logs: MutableList, 24 | ) : VmpLogger { 25 | fun start() { 26 | val simpleRules = SimpleRules().apply { 27 | parse(InputStreamReader(FileInputStream(rules), StandardCharsets.UTF_8)) 28 | } 29 | val filterConfig = if (mapping.isNotEmpty()) { 30 | ProguardMappingConfig(BasicKeepConfig(), MappingReader(File(mapping)), simpleRules) 31 | } else { 32 | SimpleConvertConfig(BasicKeepConfig(), simpleRules) 33 | } 34 | ApkProtect.Builder(ApkFolders(File(input), File(output))).apply { 35 | setInstructionRewriter(RandomInstructionRewriter()) 36 | setFilter(filterConfig) 37 | setLogger(this@ApkVmpTask) 38 | setClassAnalyzer(ClassAnalyzer()) 39 | }.build().run() 40 | } 41 | 42 | override fun info(msg: String?) { 43 | if (!msg.isNullOrEmpty()) { 44 | logs.add("I: $msg") 45 | } 46 | } 47 | 48 | override fun error(msg: String?) { 49 | if (!msg.isNullOrEmpty()) { 50 | logs.add("E: $msg") 51 | } 52 | } 53 | 54 | override fun warning(msg: String?) { 55 | if (!msg.isNullOrEmpty()) { 56 | logs.add("W: $msg") 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/utils/FilePicker.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import java.io.File 4 | import javax.swing.JFileChooser 5 | 6 | object FilePicker { 7 | fun chooseFile(description: String, baseDirectory: String): String? { 8 | val fileChooser = JFileChooser(baseDirectory).apply { 9 | fileSelectionMode = JFileChooser.FILES_ONLY 10 | dialogTitle = description 11 | approveButtonText = "Select" 12 | approveButtonToolTipText = description 13 | } 14 | fileChooser.showOpenDialog(null) 15 | val result = fileChooser.selectedFile 16 | return if (result != null && result.exists()) { 17 | result.absolutePath.toString() 18 | } else { 19 | null 20 | } 21 | } 22 | 23 | fun chooseDirectory(description: String, baseDirectory: String): String? { 24 | val fileChooser = JFileChooser(baseDirectory).apply { 25 | fileSelectionMode = JFileChooser.DIRECTORIES_ONLY 26 | dialogTitle = description 27 | approveButtonText = "Select" 28 | approveButtonToolTipText = description 29 | } 30 | fileChooser.showOpenDialog(null) 31 | val result = fileChooser.selectedFile 32 | return if (result != null && result.exists()) { 33 | result.absolutePath.toString() 34 | } else { 35 | null 36 | } 37 | } 38 | 39 | fun getParentDirectory(path: String): String { 40 | val file = File(path) 41 | return if (file.exists()) { 42 | if (file.isDirectory) { 43 | file.absolutePath 44 | } else { 45 | file.parent 46 | } 47 | } else { 48 | "/" 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /composeApp/src/desktopMain/resources/apksigner.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timscriptov/nmmp/ceb3417f756a4193dfe2ae24da27cf2a04ee1470/composeApp/src/desktopMain/resources/apksigner.jar -------------------------------------------------------------------------------- /composeApp/src/desktopMain/resources/ic_folder_open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /desktop-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class androidx.** {*;} 2 | -keep class android.** {*;} 3 | -keep class com.google.accompanist.** {*;} 4 | -dontoptimize 5 | 6 | # The Android pre-handler for exceptions is loaded reflectively (via ServiceLoader). 7 | -keep class kotlinx.coroutines.experimental.android.AndroidExceptionPreHandler.** { *; } 8 | 9 | -ignorewarnings 10 | -dontwarn org.jetbrains.** 11 | -dontnote org.jetbrains.** 12 | -dontwarn okhttp3.internal.** 13 | -dontnote okhttp3.internal.** 14 | -dontwarn kotlinx.** 15 | -dontnote kotlinx.** 16 | -dontwarn kotlin.** 17 | -dontnote kotlin.** 18 | -dontwarn com.android.apksigner.** 19 | -dontnote com.android.apksigner.** 20 | -dontwarn com.android.apksigner.** 21 | -dontnote com.android.apksigner.** -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.version=1.9.21 3 | agp.version=7.4.1 4 | compose.version=1.5.4 5 | 6 | org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 7 | 8 | development=true -------------------------------------------------------------------------------- /gradle/libs.version.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.2.1" 3 | android-compileSdk = "34" 4 | android-minSdk = "24" 5 | android-targetSdk = "34" 6 | androidx-activityCompose = "1.8.2" 7 | androidx-appcompat = "1.6.1" 8 | androidx-constraintlayout = "2.1.4" 9 | androidx-core-ktx = "1.12.0" 10 | androidx-espresso-core = "3.5.1" 11 | androidx-material = "1.11.0" 12 | androidx-test-junit = "1.1.5" 13 | compose = "1.5.4" 14 | compose-compiler = "1.5.6" 15 | compose-plugin = "1.5.11" 16 | junit = "4.13.2" 17 | kamelImage = "0.9.1" 18 | koinCompose = "1.1.2" 19 | koinCore = "3.5.3" 20 | kotlin = "1.9.21" 21 | 22 | kotlinxCoroutinesCore = "1.7.3" 23 | kotlinxSerializationJson = "1.6.2" 24 | ktorClientCore = "2.3.7" 25 | ktorClientOkhttp = "2.3.5" 26 | voyager = "1.0.0" 27 | composeShimmer = "1.2.0" 28 | 29 | [libraries] 30 | kamel-image = { module = "media.kamel:kamel-image", version.ref = "kamelImage" } 31 | koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinCompose" } 32 | koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" } 33 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 34 | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 35 | junit = { group = "junit", name = "junit", version.ref = "junit" } 36 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } 37 | androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } 38 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" } 39 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } 40 | androidx-material = { group = "com.google.android.material", name = "material", version.ref = "androidx-material" } 41 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } 42 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 43 | compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } 44 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } 45 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } 46 | compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } 47 | compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } 48 | 49 | compose-shimmer = { module = "com.valentinilk.shimmer:compose-shimmer", version.ref = "composeShimmer" } 50 | 51 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } 52 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } 53 | ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientOkhttp" } 54 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" } 55 | ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorClientOkhttp" } 56 | voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } 57 | voyager-screenModel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } 58 | voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } 59 | voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } 60 | voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" } 61 | 62 | [plugins] 63 | androidApplication = { id = "com.android.application", version.ref = "agp" } 64 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 65 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 66 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 67 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 68 | 69 | [bundles] 70 | voyager = [ "voyager-navigator", "voyager-screenModel", "voyager-tabNavigator", "voyager-transitions", "voyager-koin" ] -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | compose = "1.5.4" 3 | compose-compiler = "1.5.6" 4 | compose-plugin = "1.5.11" 5 | junit = "4.13.2" 6 | kotlin = "1.9.21" 7 | voyager = "1.0.0" 8 | composeShimmer = "1.2.0" 9 | 10 | [libraries] 11 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 12 | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 13 | junit = { group = "junit", name = "junit", version.ref = "junit" } 14 | compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } 15 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } 16 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } 17 | compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } 18 | compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } 19 | 20 | compose-shimmer = { module = "com.valentinilk.shimmer:compose-shimmer", version.ref = "composeShimmer" } 21 | 22 | voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } 23 | voyager-screenModel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } 24 | voyager-tabNavigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } 25 | voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } 26 | voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" } 27 | 28 | [plugins] 29 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 30 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 31 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 32 | 33 | [bundles] 34 | voyager = [ "voyager-navigator", "voyager-screenModel", "voyager-tabNavigator", "voyager-transitions", "voyager-koin" ] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timscriptov/nmmp/ceb3417f756a4193dfe2ae24da27cf2a04ee1470/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | before_install: 4 | - sdk install java 17.0.9-open 5 | - sdk use java 17.0.9-open -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.google.protobuf.gradle.protobuf 2 | import com.google.protobuf.gradle.protoc 3 | 4 | plugins { 5 | java 6 | id("maven-publish") 7 | kotlin("jvm") 8 | id("com.google.protobuf") version "0.8.13" 9 | } 10 | 11 | sourceSets { 12 | getByName("main").java { 13 | srcDir("build/generated/source/proto/main/java") 14 | } 15 | } 16 | 17 | dependencies { 18 | implementation("com.github.timscriptov:apkparser:1.2.7") 19 | implementation("com.github.timscriptov:preferences:1.0.4") 20 | implementation("com.android.tools.smali:smali-dexlib2:3.0.3") 21 | implementation("org.jetbrains:annotations:24.0.1") 22 | implementation("com.google.guava:guava:32.0.0-jre") 23 | implementation("com.google.code.gson:gson:2.10.1") 24 | implementation("org.ow2.asm:asm:9.5") 25 | implementation("com.android.tools:r8:8.1.56") 26 | implementation("com.google.protobuf:protobuf-java:3.22.2") 27 | implementation("com.google.protobuf:protobuf-java:3.19.6") 28 | } 29 | 30 | protobuf { 31 | protoc { 32 | artifact = "com.google.protobuf:protoc:3.19.6" 33 | } 34 | } 35 | 36 | publishing { 37 | publications { 38 | register("release") { 39 | groupId = "com.mcal" 40 | artifactId = "nmmp" 41 | version = "1.3.2" 42 | 43 | afterEvaluate { 44 | from(components["java"]) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/ApkFolders.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect; 2 | 3 | import java.io.File; 4 | 5 | public class ApkFolders { 6 | private final File inApk; 7 | private final File outApkFile; 8 | 9 | public ApkFolders(File inApk, File outApkFile) { 10 | this.inApk = inApk; 11 | this.outApkFile = outApkFile; 12 | } 13 | 14 | public File getInApk() { 15 | return inApk; 16 | } 17 | 18 | public File getOutRootDir() { 19 | return outApkFile.getParentFile(); 20 | } 21 | 22 | //apk解压目录,生成新apk之后可以删除 23 | public File getZipExtractTempDir() { 24 | return new File(getOutRootDir(), ".apk_temp"); 25 | } 26 | 27 | //c源代码输出目录 28 | public File getDex2cSrcDir() { 29 | return new File(getOutRootDir(), "dex2c"); 30 | } 31 | 32 | //c文件及对应dex输出目录 33 | public File getCodeGeneratedDir() { 34 | return new File(getDex2cSrcDir(), "generated"); 35 | } 36 | 37 | //在处理过的classes.dex里插入jni初始化代码及主classes.dex加载so库代码,生成新dex输出目录 38 | //这个目录可以删除,但是为了更好debug之类就保留了方便查看dex 39 | public File getTempDexDir() { 40 | return new File(getOutRootDir(), "dex_output"); 41 | } 42 | 43 | public File getOutputApk() { 44 | return outApkFile; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/aab/AabFolders.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.aab; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.File; 6 | 7 | public class AabFolders { 8 | @NotNull 9 | private final File inAab; 10 | @NotNull 11 | private final File outputAabFile; 12 | 13 | public AabFolders(@NotNull File inAab, @NotNull File outputAabFile) { 14 | this.inAab = inAab; 15 | this.outputAabFile = outputAabFile; 16 | } 17 | 18 | @NotNull 19 | public File getInAab() { 20 | return inAab; 21 | } 22 | 23 | @NotNull 24 | public File getOutRootDir() { 25 | return outputAabFile.getParentFile(); 26 | } 27 | 28 | //apk解压目录,生成新apk之后可以删除 29 | @NotNull 30 | public File getZipExtractTempDir() { 31 | return new File(getOutRootDir(), ".apk_temp"); 32 | } 33 | 34 | //c源代码输出目录 35 | @NotNull 36 | public File getDex2cSrcDir() { 37 | return new File(getOutRootDir(), "dex2c"); 38 | } 39 | 40 | //c文件及对应dex输出目录 41 | @NotNull 42 | public File getCodeGeneratedDir() { 43 | return new File(getDex2cSrcDir(), "generated"); 44 | } 45 | 46 | //在处理过的classes.dex里插入jni初始化代码及主classes.dex加载so库代码,生成新dex输出目录 47 | //这个目录可以删除,但是为了更好debug之类就保留了方便查看dex 48 | @NotNull 49 | public File getTempDexDir() { 50 | return new File(getOutRootDir(), "dex_output"); 51 | } 52 | 53 | 54 | @NotNull 55 | public File getOutputAab() { 56 | return outputAabFile; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/aab/proto/ProtoUtils.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.aab.proto; 2 | 3 | import com.android.aapt.Resources; 4 | import com.android.bundle.Config; 5 | import com.android.bundle.Files; 6 | import com.android.bundle.Targeting; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.io.OutputStream; 12 | import java.util.List; 13 | 14 | public class ProtoUtils { 15 | 16 | /** 17 | * BundleConfig.pb文件 18 | */ 19 | public static class BundleConfig { 20 | /** 21 | * 修改BundleConfig.pb一些属性 22 | * 23 | * @param configBytes 24 | * @return 25 | * @throws IOException 26 | */ 27 | public static byte @NotNull [] editConfig(byte @NotNull [] configBytes) throws IOException { 28 | final Config.BundleConfig.Builder configBuilder = Config.BundleConfig.parseFrom(configBytes).toBuilder(); 29 | final Config.Optimizations.Builder optimizationsBuilder = configBuilder.getOptimizationsBuilder(); 30 | 31 | //未压缩本地库设置 32 | //设置为true则可以在生成对应abi的apk不压缩,从而减小安装后体积 33 | final Config.UncompressNativeLibraries.Builder uncompressNativeLibBuilder = Config.UncompressNativeLibraries.newBuilder(); 34 | uncompressNativeLibBuilder.setEnabled(false); 35 | optimizationsBuilder.setUncompressNativeLibraries(uncompressNativeLibBuilder); 36 | 37 | final ByteArrayOutputStream bout = new ByteArrayOutputStream(configBytes.length); 38 | configBuilder.build().writeTo(bout); 39 | return bout.toByteArray(); 40 | } 41 | } 42 | 43 | /** 44 | * .aab内的AndroidManifest.xml是protobuf生成的二进制文件 45 | */ 46 | public static class AndroidManifest { 47 | public static @NotNull String getPackageName(byte @NotNull [] manifestBytes) throws IOException { 48 | final Resources.XmlNode xmlNode = Resources.XmlNode.parseFrom(manifestBytes); 49 | final Resources.XmlElement element = xmlNode.getElement(); 50 | if (!"manifest".equals(element.getName())) { 51 | throw new IOException("Not is manifest"); 52 | } 53 | final int count = element.getAttributeCount(); 54 | for (int i = 0; i < count; i++) { 55 | final Resources.XmlAttribute attr = element.getAttribute(i); 56 | if ("package".equals(attr.getName())) { 57 | return attr.getValue(); 58 | } 59 | } 60 | throw new IOException("No package attr"); 61 | } 62 | 63 | /** 64 | * 修改原Manifest里一些属性 65 | * 66 | * @param manifestBytes 67 | * @return 68 | * @throws IOException 69 | */ 70 | public static byte @NotNull [] editAndroidManifest(byte @NotNull [] manifestBytes) throws IOException { 71 | final Resources.XmlNode.Builder rootNodeBuilder = Resources.XmlNode.parseFrom(manifestBytes).toBuilder(); 72 | 73 | final Resources.XmlElement.Builder elementBuilder = rootNodeBuilder.getElementBuilder(); 74 | if (!"manifest".equals(elementBuilder.getName())) { 75 | throw new IOException("Not is manifest"); 76 | } 77 | 78 | final int count = elementBuilder.getChildCount(); 79 | for (int i = 0; i < count; i++) { 80 | final Resources.XmlNode.Builder childBuilder = elementBuilder.getChildBuilder(i); 81 | final Resources.XmlElement.Builder childElementBuilder; 82 | if (childBuilder.hasElement() && "application".equals((childElementBuilder = childBuilder.getElementBuilder()).getName())) { 83 | final int attrCount = childElementBuilder.getAttributeCount(); 84 | //查找extractNativeLibs 属性,如果找到则删除它 85 | //todo 实际测试后发现这个属性无用会被覆盖,需要修改BundleConfig.pb 86 | int attrIdx = -1; 87 | for (int j = 0; j < attrCount; j++) { 88 | final Resources.XmlAttribute.Builder attribute = childElementBuilder.getAttributeBuilder(j); 89 | if ("extractNativeLibs".equals(attribute.getName())) { 90 | attrIdx = j; 91 | } 92 | } 93 | if (attrIdx != -1) { 94 | childElementBuilder.removeAttribute(attrIdx); 95 | } 96 | } 97 | } 98 | final ByteArrayOutputStream bout = new ByteArrayOutputStream(manifestBytes.length); 99 | rootNodeBuilder.build().writeTo(bout); 100 | return bout.toByteArray(); 101 | } 102 | } 103 | 104 | /** 105 | * base/native.pb文件生成 106 | */ 107 | public static class NativeLibraries { 108 | 109 | private static Files.@NotNull NativeLibraries genNativeLibsProtoBuf(@NotNull List abis) { 110 | 111 | final Files.NativeLibraries.Builder nativeLibsBuilder = Files.NativeLibraries.newBuilder(); 112 | 113 | for (String abi : abis) { 114 | final Targeting.NativeDirectoryTargeting.Builder targetingBuilder = Targeting.NativeDirectoryTargeting.newBuilder(); 115 | final Files.TargetedNativeDirectory.Builder dirBuilder = Files.TargetedNativeDirectory.newBuilder(); 116 | if ("armeabi-v7a".equals(abi)) { 117 | targetingBuilder.getAbiBuilder().setAlias(Targeting.Abi.AbiAlias.ARMEABI_V7A); 118 | } else if ("arm64-v8a".equals(abi)) { 119 | targetingBuilder.getAbiBuilder().setAlias(Targeting.Abi.AbiAlias.ARM64_V8A); 120 | } else if ("x86".equals(abi)) { 121 | targetingBuilder.getAbiBuilder().setAlias(Targeting.Abi.AbiAlias.X86); 122 | } else if ("x86_64".equals(abi)) { 123 | targetingBuilder.getAbiBuilder().setAlias(Targeting.Abi.AbiAlias.X86_64); 124 | } else { 125 | throw new RuntimeException("Unknown abi " + abi); 126 | } 127 | dirBuilder.setPath("lib/" + abi); 128 | dirBuilder.setTargeting(targetingBuilder); 129 | nativeLibsBuilder.addDirectory(dirBuilder); 130 | } 131 | return nativeLibsBuilder.build(); 132 | } 133 | 134 | public static void writeNativePB(@NotNull List abis, @NotNull OutputStream out) throws IOException { 135 | final Files.NativeLibraries nativeLibraries = genNativeLibsProtoBuf(abis); 136 | nativeLibraries.writeTo(out); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/aar/AarFolders.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.aar; 2 | 3 | import com.nmmedit.apkprotect.ApkFolders; 4 | 5 | import java.io.File; 6 | 7 | public class AarFolders { 8 | public final ApkFolders apkFolders; 9 | private final File inputAarFile; 10 | private final File outputAarFile; 11 | 12 | public AarFolders(File aar, File outputAarFile) { 13 | this.inputAarFile = aar; 14 | this.outputAarFile = outputAarFile; 15 | this.apkFolders = new ApkFolders(getConvertedDexJar(), outputAarFile); 16 | } 17 | 18 | public File getInputAar() { 19 | return inputAarFile; 20 | } 21 | 22 | public File getOutputDir() { 23 | return outputAarFile.getParentFile(); 24 | } 25 | 26 | 27 | public File getConvertedDexJar() { 28 | return new File(getTempDir(), getOutputDir().getName() + "-dex.jar"); 29 | } 30 | 31 | public File getTempDir() { 32 | final File file = new File(getOutputDir(), ".dx_temp"); 33 | if (!file.exists()) file.mkdirs(); 34 | return file; 35 | } 36 | 37 | public File getOutputAar() { 38 | return outputAarFile; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/aar/asm/AsmMethod.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.aar.asm; 2 | 3 | public class AsmMethod { 4 | public final int access; 5 | public final String name; 6 | public final String descriptor; 7 | 8 | public AsmMethod(int access, String name, String descriptor) { 9 | this.access = access; 10 | this.name = name; 11 | this.descriptor = descriptor; 12 | } 13 | 14 | @Override 15 | public boolean equals(Object o) { 16 | if (this == o) return true; 17 | if (o == null || getClass() != o.getClass()) return false; 18 | 19 | AsmMethod myMethod = (AsmMethod) o; 20 | 21 | if (!name.equals(myMethod.name)) return false; 22 | return descriptor.equals(myMethod.descriptor); 23 | } 24 | 25 | @Override 26 | public int hashCode() { 27 | int result = name.hashCode(); 28 | result = 31 * result + descriptor.hashCode(); 29 | return result; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/aar/asm/AsmUtils.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.aar.asm; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.objectweb.asm.ClassWriter; 5 | import org.objectweb.asm.MethodVisitor; 6 | import org.objectweb.asm.Opcodes; 7 | 8 | import java.util.List; 9 | 10 | public class AsmUtils { 11 | 12 | //生成NativeUtils类, 用于实现lib加载及类初始化 13 | public static byte[] genCfNativeUtil(@NotNull String clsName, 14 | @NotNull String libName, 15 | @NotNull List initMethodNames) { 16 | final ClassWriter cw = new ClassWriter(0); 17 | 18 | cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL, 19 | clsName, null, "java/lang/Object", null); 20 | 21 | // static{ 22 | // System.loadLibrary("libname"); 23 | // } 24 | MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, 25 | "", "()V", 26 | null, null); 27 | mv.visitMaxs(1, 1); 28 | mv.visitLdcInsn(libName); 29 | mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "loadLibrary", "(Ljava/lang/String;)V", false); 30 | mv.visitInsn(Opcodes.RETURN); 31 | mv.visitEnd(); 32 | 33 | mv = cw.visitMethod(Opcodes.ACC_PRIVATE, 34 | "", "()V", 35 | null, null); 36 | mv.visitMaxs(1, 1); 37 | mv.visitVarInsn(Opcodes.ALOAD, 0); 38 | mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); 39 | mv.visitInsn(Opcodes.RETURN); 40 | mv.visitEnd(); 41 | 42 | 43 | //native void classInit0(int idx); 44 | for (String methodName : initMethodNames) { 45 | cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_NATIVE, methodName, "(I)V", null, null); 46 | } 47 | cw.visitEnd(); 48 | 49 | return cw.toByteArray(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/aar/asm/InjectStaticBlockVisitor.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.aar.asm; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.objectweb.asm.ClassVisitor; 5 | import org.objectweb.asm.MethodVisitor; 6 | import org.objectweb.asm.Opcodes; 7 | 8 | /** 9 | * 在原class插入初始化代码 10 | */ 11 | public class InjectStaticBlockVisitor extends ClassVisitor { 12 | private final String typeName; 13 | private final String methodName; 14 | private final int classIdx; 15 | 16 | private boolean hasStaticBlock; 17 | 18 | public InjectStaticBlockVisitor(int api, 19 | ClassVisitor classVisitor, 20 | String typeName, 21 | String methodName, 22 | int classIdx) { 23 | super(api, classVisitor); 24 | this.typeName = typeName; 25 | this.methodName = methodName; 26 | this.classIdx = classIdx; 27 | } 28 | 29 | // NativeUtils.classesInit(idx); 30 | // 方法签名固定为(I)V,就类名跟方法名可变 31 | private static void genCallClassesInit(@NotNull MethodVisitor mv, 32 | String clsName, 33 | String initMethodName, 34 | int idx) { 35 | //选择更适合的索引加载指令 36 | if (idx < 0) {//unsigned int 37 | mv.visitLdcInsn(idx); 38 | } else if (idx <= Byte.MAX_VALUE) { 39 | mv.visitIntInsn(Opcodes.BIPUSH, idx); 40 | } else if (idx <= Short.MAX_VALUE) { 41 | mv.visitIntInsn(Opcodes.SIPUSH, idx); 42 | } else { 43 | mv.visitLdcInsn(idx); 44 | } 45 | //调用classesInit方法 46 | mv.visitMethodInsn(Opcodes.INVOKESTATIC, clsName, initMethodName, "(I)V", false); 47 | } 48 | 49 | @Override 50 | public MethodVisitor visitMethod(int access, String name, String descriptor, 51 | String signature, String[] exceptions) { 52 | if ("".equals(name)) { 53 | hasStaticBlock = true; 54 | final MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); 55 | return new MethodVisitor(api, mv) { 56 | @Override 57 | public void visitCode() { 58 | super.visitCode(); 59 | //原class存在静态块,只需要在最前面插入调用初始化方法代码 60 | genCallClassesInit(this, typeName, methodName, classIdx); 61 | } 62 | }; 63 | } 64 | return super.visitMethod(access, name, descriptor, signature, exceptions); 65 | } 66 | 67 | @Override 68 | public void visitEnd() { 69 | super.visitEnd(); 70 | if (!hasStaticBlock) {//原class没有静态块,需要添加 71 | if (cv != null) { 72 | final MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "", "()V", null, null); 73 | if (mv != null) { 74 | mv.visitCode(); 75 | 76 | genCallClassesInit(mv, typeName, methodName, classIdx); 77 | 78 | mv.visitInsn(Opcodes.RETURN); 79 | mv.visitEnd(); 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/aar/asm/MethodToNativeVisitor.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.aar.asm; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.objectweb.asm.ClassVisitor; 5 | import org.objectweb.asm.MethodVisitor; 6 | import org.objectweb.asm.Opcodes; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * 把需要转换的方法标识为native,去掉字节码部分 13 | */ 14 | public class MethodToNativeVisitor extends ClassVisitor { 15 | private final Map> convertedMethods; 16 | 17 | public MethodToNativeVisitor(int api, 18 | ClassVisitor classVisitor, 19 | Map> convertedMethods) { 20 | super(api, classVisitor); 21 | this.convertedMethods = convertedMethods; 22 | } 23 | 24 | //生成第一个方法调用第二个的字节码 25 | private static void genCall(ClassVisitor cv, @NotNull AsmMethod method1, @NotNull AsmMethod method2) { 26 | final String sig1 = method1.descriptor; 27 | final String sig2 = method2.descriptor; 28 | if (!sig1.equals(sig2)) { 29 | throw new IllegalStateException(); 30 | } 31 | final MethodVisitor mv = cv.visitMethod(method1.access, method1.name, sig1, null, null); 32 | 33 | } 34 | 35 | private static void genLoadInsn(MethodVisitor mv, int slotIdx, String methodName, String sig) { 36 | 37 | } 38 | 39 | @Override 40 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 41 | final List myMethods = convertedMethods.get(new AsmMethod(access, name, descriptor)); 42 | if (myMethods != null && !myMethods.isEmpty()) { 43 | if (myMethods.size() == 1) { 44 | final MethodVisitor mv = super.visitMethod(access | Opcodes.ACC_NATIVE, name, descriptor, signature, exceptions); 45 | //不需要code等部分 46 | return null; 47 | } 48 | // myMethods.size()==2 49 | // todo 生成第一个方法调用第二个方法代码,解决一些native方法无法正常初始化问题。 50 | } 51 | return super.visitMethod(access, name, descriptor, signature, exceptions); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/data/Prefs.kt: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.data 2 | 3 | import com.mcal.preferences.PreferencesManager 4 | import com.nmmedit.apkprotect.data.Storage.binDir 5 | import com.nmmedit.apkprotect.util.OsDetector 6 | 7 | object Prefs : PreferencesManager(binDir, "nmmp_preferences.json") { 8 | @JvmStatic 9 | fun isArm(): Boolean { 10 | return getBoolean("arm", true) 11 | } 12 | 13 | @JvmStatic 14 | fun setArm(mode: Boolean) { 15 | putBoolean("arm", mode) 16 | } 17 | 18 | @JvmStatic 19 | fun isArm64(): Boolean { 20 | return getBoolean("arm64", true) 21 | } 22 | 23 | @JvmStatic 24 | fun setArm64(mode: Boolean) { 25 | putBoolean("arm64", mode) 26 | } 27 | 28 | @JvmStatic 29 | fun isX86(): Boolean { 30 | return getBoolean("x86", true) 31 | } 32 | 33 | @JvmStatic 34 | fun setX86(mode: Boolean) { 35 | putBoolean("x86", mode) 36 | } 37 | 38 | @JvmStatic 39 | fun isX64(): Boolean { 40 | return getBoolean("x64", true) 41 | } 42 | 43 | @JvmStatic 44 | fun setX64(mode: Boolean) { 45 | putBoolean("x64", mode) 46 | } 47 | 48 | @JvmStatic 49 | fun getVmName(): String { 50 | return getString("vm_name", "nmmvm") 51 | } 52 | 53 | @JvmStatic 54 | fun setVmName(name: String) { 55 | putString("vm_name", name) 56 | } 57 | 58 | @JvmStatic 59 | fun getNmmpName(): String { 60 | return getString("nmmp_name", "nmmp") 61 | } 62 | 63 | @JvmStatic 64 | fun setNmmpName(name: String) { 65 | putString("nmmp_name", name) 66 | } 67 | 68 | @JvmStatic 69 | fun setCxxFlags(flags: String) { 70 | putString("cxx_flags", flags) 71 | } 72 | 73 | @JvmStatic 74 | fun getCxxFlags(): String { 75 | return getString("cxx_flags", "") 76 | } 77 | 78 | @JvmStatic 79 | fun getRegisterNativesClassName(): String { 80 | return getString( 81 | "register_natives_class_name", 82 | "com/nmmedit/protect/NativeUtil" 83 | ) 84 | } 85 | 86 | @JvmStatic 87 | fun setRegisterNativesClassName(path: String) { 88 | putString("register_natives_class_name", path) 89 | } 90 | 91 | @JvmStatic 92 | fun getSdkPath(): String { 93 | return getString("sdk_path", System.getenv("ANDROID_SDK_HOME") ?: "") 94 | } 95 | 96 | @JvmStatic 97 | fun setSdkPath(path: String) { 98 | putString("sdk_path", path) 99 | } 100 | 101 | @JvmStatic 102 | fun getCmakePath(): String { 103 | return getString("cmake_path", System.getenv("CMAKE_PATH") ?: "") 104 | } 105 | 106 | @JvmStatic 107 | fun setCmakePath(path: String) { 108 | putString("cmake_path", path) 109 | } 110 | 111 | @JvmStatic 112 | fun getNdkPath(): String { 113 | return getString("ndk_path", System.getenv("ANDROID_NDK_HOME") ?: "") 114 | } 115 | 116 | @JvmStatic 117 | fun setNdkPath(path: String) { 118 | putString("ndk_path", path) 119 | } 120 | 121 | @JvmStatic 122 | fun getNdkToolchains(): String { 123 | return getString("toolchains", "/toolchains/llvm/prebuilt/") 124 | } 125 | 126 | @JvmStatic 127 | fun getNdkAbi(): String { 128 | return getString( 129 | "abi", 130 | if (OsDetector.isWindows) { 131 | "windows-x86_64" 132 | } else { 133 | "linux-x86_64" 134 | } 135 | ) 136 | } 137 | 138 | @JvmStatic 139 | fun getNdkStrip(): String { 140 | return getString("strip", "/bin/llvm-strip") 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/data/Storage.kt: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.data 2 | 3 | import com.nmmedit.apkprotect.util.FileHelper 4 | import java.io.File 5 | 6 | object Storage { 7 | @JvmStatic 8 | val workingDir: File 9 | get() { 10 | return File(File(FileHelper::class.java.protectionDomain.codeSource.location.toURI().path).parentFile.path) 11 | } 12 | 13 | @JvmStatic 14 | val binDir: File 15 | get() { 16 | val dir = File(workingDir, "bin") 17 | if (!dir.exists()) { 18 | dir.mkdirs() 19 | } 20 | return dir 21 | } 22 | } -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/deobfus/MappingProcessor.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.deobfus; 2 | 3 | public interface MappingProcessor { 4 | /** 5 | * Processes the given class name mapping. 6 | * 7 | * @param className the original class name. 8 | * @param newClassName the new class name. 9 | * @return whether the processor is interested in receiving mappings of the 10 | * class members of this class. 11 | */ 12 | void processClassMapping(String className, 13 | String newClassName); 14 | 15 | /** 16 | * Processes the given field name mapping. 17 | * 18 | * @param className the original class name. 19 | * @param fieldType the original external field type. 20 | * @param fieldName the original field name. 21 | * @param newClassName the new class name. 22 | * @param newFieldName the new field name. 23 | */ 24 | void processFieldMapping(String className, 25 | String fieldType, 26 | String fieldName, 27 | String newClassName, 28 | String newFieldName); 29 | 30 | /** 31 | * Processes the given method name mapping. 32 | * 33 | * @param className the original class name. 34 | * @param firstLineNumber the first line number of the method, or 0 if 35 | * it is not known. 36 | * @param lastLineNumber the last line number of the method, or 0 if 37 | * it is not known. 38 | * @param methodReturnType the original external method return type. 39 | * @param methodName the original external method name. 40 | * @param methodArguments the original external method arguments. 41 | * @param newClassName the new class name. 42 | * @param newFirstLineNumber the new first line number of the method, or 0 43 | * if it is not known. 44 | * @param newLastLineNumber the new last line number of the method, or 0 45 | * if it is not known. 46 | * @param newMethodName the new method name. 47 | */ 48 | void processMethodMapping(String className, 49 | int firstLineNumber, 50 | int lastLineNumber, 51 | String methodReturnType, 52 | String methodName, 53 | String methodArguments, 54 | String newClassName, 55 | int newFirstLineNumber, 56 | int newLastLineNumber, 57 | String newMethodName); 58 | } 59 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/deobfus/MappingReader.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.deobfus; 2 | 3 | import org.jetbrains.annotations.Contract; 4 | import org.jetbrains.annotations.NotNull; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import java.io.*; 8 | import java.nio.charset.StandardCharsets; 9 | 10 | public class MappingReader { 11 | private final File mappingFile; 12 | private final InputStream mappingInput; 13 | 14 | public MappingReader(File mappingFile) { 15 | this.mappingFile = mappingFile; 16 | this.mappingInput = null; 17 | } 18 | 19 | public MappingReader(InputStream mappingInput) { 20 | this.mappingInput = mappingInput; 21 | this.mappingFile = null; 22 | } 23 | 24 | @Contract(" -> new") 25 | private @NotNull Reader getMappingReader() throws IOException { 26 | if (mappingFile != null) { 27 | return new FileReader(mappingFile); 28 | } 29 | if (mappingInput != null) { 30 | return new InputStreamReader(mappingInput, StandardCharsets.UTF_8); 31 | } 32 | throw new IOException("No mapping"); 33 | } 34 | 35 | public void parse(MappingProcessor processor) throws IOException { 36 | try (BufferedReader reader = new BufferedReader(getMappingReader()) 37 | ) { 38 | String className = null; 39 | String line; 40 | while ((line = reader.readLine()) != null) { 41 | line = line.trim(); 42 | if (line.startsWith("#")) { 43 | continue; 44 | } 45 | if (line.endsWith(":")) { 46 | className = processClassMapping(line, processor); 47 | } else if (className != null) { 48 | processClassMemberMapping(className, line, processor); 49 | } 50 | } 51 | 52 | } 53 | } 54 | 55 | /** 56 | * Parses the given line with a class mapping and processes the 57 | * results with the given mapping processor. Returns the old class name, 58 | * or null if any subsequent class member lines can be ignored. 59 | */ 60 | private @Nullable String processClassMapping(@NotNull String line, 61 | MappingProcessor mappingProcessor) { 62 | // See if we can parse "___ -> ___:", containing the original 63 | // class name and the new class name. 64 | 65 | int arrowIndex = line.indexOf("->"); 66 | if (arrowIndex < 0) { 67 | return null; 68 | } 69 | 70 | int colonIndex = line.indexOf(':', arrowIndex + 2); 71 | if (colonIndex < 0) { 72 | return null; 73 | } 74 | 75 | // Extract the elements. 76 | String className = line.substring(0, arrowIndex).trim(); 77 | String newClassName = line.substring(arrowIndex + 2, colonIndex).trim(); 78 | 79 | // Process this class name mapping. 80 | mappingProcessor.processClassMapping(className, newClassName); 81 | 82 | return className; 83 | } 84 | 85 | /** 86 | * Parses the given line with a class member mapping and processes the 87 | * results with the given mapping processor. 88 | */ 89 | private void processClassMemberMapping(String className, 90 | @NotNull String line, 91 | MappingProcessor mappingProcessor) { 92 | // See if we can parse one of 93 | // ___ ___ -> ___ 94 | // ___:___:___ ___(___) -> ___ 95 | // ___:___:___ ___(___):___ -> ___ 96 | // ___:___:___ ___(___):___:___ -> ___ 97 | // containing the optional line numbers, the return type, the original 98 | // field/method name, optional arguments, the optional original line 99 | // numbers, and the new field/method name. The original field/method 100 | // name may contain an original class name "___.___". 101 | 102 | int colonIndex1 = line.indexOf(':'); 103 | int colonIndex2 = colonIndex1 < 0 ? -1 : line.indexOf(':', colonIndex1 + 1); 104 | int spaceIndex = line.indexOf(' ', colonIndex2 + 2); 105 | int argumentIndex1 = line.indexOf('(', spaceIndex + 1); 106 | int argumentIndex2 = argumentIndex1 < 0 ? -1 : line.indexOf(')', argumentIndex1 + 1); 107 | int colonIndex3 = argumentIndex2 < 0 ? -1 : line.indexOf(':', argumentIndex2 + 1); 108 | int colonIndex4 = colonIndex3 < 0 ? -1 : line.indexOf(':', colonIndex3 + 1); 109 | int arrowIndex = line.indexOf("->", (colonIndex4 >= 0 ? colonIndex4 : 110 | colonIndex3 >= 0 ? colonIndex3 : 111 | argumentIndex2 >= 0 ? argumentIndex2 : 112 | spaceIndex) + 1); 113 | 114 | if (spaceIndex < 0 || 115 | arrowIndex < 0) { 116 | return; 117 | } 118 | 119 | // Extract the elements. 120 | String type = line.substring(colonIndex2 + 1, spaceIndex).trim(); 121 | String name = line.substring(spaceIndex + 1, argumentIndex1 >= 0 ? argumentIndex1 : arrowIndex).trim(); 122 | String newName = line.substring(arrowIndex + 2).trim(); 123 | 124 | // Does the method name contain an explicit original class name? 125 | String newClassName = className; 126 | int dotIndex = name.lastIndexOf('.'); 127 | if (dotIndex >= 0) { 128 | className = name.substring(0, dotIndex); 129 | name = name.substring(dotIndex + 1); 130 | } 131 | 132 | // Process this class member mapping. 133 | if (type.length() > 0 && 134 | name.length() > 0 && 135 | newName.length() > 0) { 136 | // Is it a field or a method? 137 | if (argumentIndex2 < 0) { 138 | mappingProcessor.processFieldMapping(className, 139 | type, 140 | name, 141 | newClassName, 142 | newName); 143 | } else { 144 | int firstLineNumber = 0; 145 | int lastLineNumber = 0; 146 | int newFirstLineNumber = 0; 147 | int newLastLineNumber = 0; 148 | 149 | if (colonIndex2 >= 0) { 150 | firstLineNumber = newFirstLineNumber = Integer.parseInt(line.substring(0, colonIndex1).trim()); 151 | lastLineNumber = newLastLineNumber = Integer.parseInt(line.substring(colonIndex1 + 1, colonIndex2).trim()); 152 | } 153 | 154 | if (colonIndex3 >= 0) { 155 | firstLineNumber = Integer.parseInt(line.substring(colonIndex3 + 1, colonIndex4 > 0 ? colonIndex4 : arrowIndex).trim()); 156 | lastLineNumber = colonIndex4 < 0 ? firstLineNumber : 157 | Integer.parseInt(line.substring(colonIndex4 + 1, arrowIndex).trim()); 158 | } 159 | 160 | String arguments = line.substring(argumentIndex1 + 1, argumentIndex2).trim(); 161 | 162 | mappingProcessor.processMethodMapping(className, 163 | firstLineNumber, 164 | lastLineNumber, 165 | type, 166 | name, 167 | arguments, 168 | newClassName, 169 | newFirstLineNumber, 170 | newLastLineNumber, 171 | newName); 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/DexConfig.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c; 2 | 3 | import com.android.tools.smali.dexlib2.iface.Method; 4 | import com.google.common.collect.HashMultimap; 5 | import com.nmmedit.apkprotect.data.Prefs; 6 | import com.nmmedit.apkprotect.dex2c.converter.JniCodeGenerator; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | import java.io.File; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Set; 13 | 14 | /** 15 | * dex处理后生成新dex及c代码,文件名结构配置 16 | */ 17 | public class DexConfig { 18 | private final File outputDir; 19 | private final String dexName; 20 | 21 | //记录壳dex被处理过的类及处理后的方法, 因为可能新增方法所以壳dex方法跟需要转换的dex并不相等 22 | //单独记录下来,主要后面交给java-asm处理转换前的class文件 23 | private HashMultimap> shellMethods; 24 | 25 | //jnicodegenerator 处理完成后,缓存已处理的类及方法 26 | private Set handledNativeClasses; 27 | private Map nativeMethodOffsets; 28 | 29 | public DexConfig(File outputDir, @NotNull String dexFileName) { 30 | this.outputDir = outputDir; 31 | int i = dexFileName.lastIndexOf('.'); 32 | if (i != -1) { 33 | this.dexName = dexFileName.substring(0, i); 34 | } else { 35 | this.dexName = dexFileName; 36 | } 37 | } 38 | 39 | public File getOutputDir() { 40 | return outputDir; 41 | } 42 | 43 | public String getDexName() { 44 | return dexName; 45 | } 46 | 47 | //每个处理过的class,需要调用这个类里的注册函数,注册函数名和classes.dex相关 48 | public String getRegisterNativesClassName() { 49 | return Prefs.getRegisterNativesClassName(); 50 | } 51 | 52 | public String getRegisterNativesMethodName() { 53 | return getDexName() + "Init0"; 54 | } 55 | 56 | @NotNull 57 | public Set getHandledNativeClasses() { 58 | return handledNativeClasses; 59 | } 60 | 61 | public int getOffsetFromClassName(String className) { 62 | return nativeMethodOffsets.get(className); 63 | } 64 | 65 | public void setResult(@NotNull JniCodeGenerator codeGenerator) { 66 | handledNativeClasses = codeGenerator.getHandledNativeClasses(); 67 | nativeMethodOffsets = codeGenerator.getNativeMethodOffsets(); 68 | } 69 | 70 | public HashMultimap> getShellMethods() { 71 | return shellMethods; 72 | } 73 | 74 | public void setShellMethods(HashMultimap> shellMethods) { 75 | this.shellMethods = shellMethods; 76 | } 77 | 78 | /** 79 | * 方法被标识为native的dex,用于替换原dex 80 | */ 81 | public File getShellDexFile() { 82 | return new File(outputDir, dexName + "_shell.dex"); 83 | } 84 | 85 | /** 86 | * 符号及方法实现dex文件,用于生成c代码 87 | */ 88 | public File getImplDexFile() { 89 | return new File(outputDir, dexName + "_impl.dex"); 90 | } 91 | 92 | /** 93 | * 本地方法实现 94 | */ 95 | public File getNativeFunctionsFile() { 96 | return new File(outputDir, dexName + "_native_functions.c"); 97 | } 98 | 99 | /** 100 | * 初始化代码头文件及初始化函数名,提供函数给外部调用 101 | */ 102 | public HeaderFileAndSetupFuncName getHeaderFileAndSetupFunc() { 103 | return new HeaderFileAndSetupFuncName( 104 | new File(outputDir, dexName + "_native_functions.h"), 105 | dexName + "_setup"); 106 | 107 | } 108 | 109 | /** 110 | * 符号解析器代码文件 111 | */ 112 | public File getResolverFile() { 113 | return new File(outputDir, dexName + "_resolver.c"); 114 | } 115 | 116 | public static class HeaderFileAndSetupFuncName { 117 | public final File headerFile; 118 | public final String setupFunctionName; 119 | 120 | private HeaderFileAndSetupFuncName(File headerFile, String setupFunctionName) { 121 | this.headerFile = headerFile; 122 | this.setupFunctionName = setupFunctionName; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/GlobalDexConfig.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c; 2 | 3 | import java.io.File; 4 | import java.io.FileWriter; 5 | import java.io.IOException; 6 | import java.io.Writer; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class GlobalDexConfig { 11 | 12 | private final ArrayList configs = new ArrayList<>(); 13 | 14 | private final File outputDir; 15 | 16 | public GlobalDexConfig(File outputDir) { 17 | this.outputDir = outputDir; 18 | } 19 | 20 | public File getInitCodeFile() { 21 | return new File(outputDir, "jni_init.c"); 22 | } 23 | 24 | public void addDexConfig(DexConfig config) { 25 | configs.add(config); 26 | } 27 | 28 | public List getConfigs() { 29 | return configs; 30 | } 31 | 32 | public void generateJniInitCode() throws IOException { 33 | try ( 34 | final FileWriter writer = new FileWriter(getInitCodeFile()); 35 | ) { 36 | generateJniInitCode(writer); 37 | } 38 | } 39 | 40 | private void generateJniInitCode(Writer writer) throws IOException { 41 | final StringBuilder includeStaOrExternFunc = new StringBuilder(); 42 | 43 | final StringBuilder initCallSta = new StringBuilder(); 44 | 45 | for (DexConfig config : configs) { 46 | final DexConfig.HeaderFileAndSetupFuncName setupFunc = config.getHeaderFileAndSetupFunc(); 47 | includeStaOrExternFunc.append(String.format("extern void %s(JNIEnv *env);\n", setupFunc.setupFunctionName)); 48 | initCallSta.append(String.format(" %s(env);\n", setupFunc.setupFunctionName)); 49 | } 50 | 51 | writer.write(String.format( 52 | "#include \n" + 53 | "#include \"GlobalCache.h\"\n" + 54 | "\n" + 55 | "//auto generated\n" + 56 | "%s" + 57 | "\n" + 58 | "\n" + 59 | "JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {\n" + 60 | " JNIEnv *env;\n" + 61 | " if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {\n" + 62 | " return -1;\n" + 63 | " }\n" + 64 | " cacheInitial(env);\n" + 65 | "\n" + 66 | "\n" + 67 | " //auto generated setup function\n" + 68 | "%s" + 69 | "\n" + 70 | "\n" + 71 | " return JNI_VERSION_1_6;\n" + 72 | "}\n\n\n", 73 | includeStaOrExternFunc.toString(), initCallSta.toString())); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/ClassAnalyzer.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter; 2 | 3 | import com.android.tools.smali.dexlib2.Opcodes; 4 | import com.android.tools.smali.dexlib2.dexbacked.DexBackedClassDef; 5 | import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile; 6 | import com.android.tools.smali.dexlib2.iface.ClassDef; 7 | import com.android.tools.smali.dexlib2.iface.Field; 8 | import com.android.tools.smali.dexlib2.iface.Method; 9 | import com.android.tools.smali.dexlib2.iface.MethodImplementation; 10 | import com.android.tools.smali.dexlib2.iface.instruction.Instruction; 11 | import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction; 12 | import com.android.tools.smali.dexlib2.iface.reference.FieldReference; 13 | import com.android.tools.smali.dexlib2.iface.reference.MethodReference; 14 | import com.android.tools.smali.dexlib2.immutable.reference.ImmutableFieldReference; 15 | import com.google.common.collect.Maps; 16 | import org.jetbrains.annotations.NotNull; 17 | import org.jetbrains.annotations.Nullable; 18 | 19 | import java.io.BufferedInputStream; 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.IOException; 23 | import java.util.List; 24 | import java.util.Map; 25 | 26 | //目前用于分析接口静态域,后面可能其他用途 27 | //需要添加多个dex文件,以便完整分析class 28 | public class ClassAnalyzer { 29 | private final Map allClasses = Maps.newHashMap(); 30 | 31 | private boolean hasJna; 32 | 33 | private int minSdk = 21; 34 | 35 | public ClassAnalyzer() { 36 | } 37 | 38 | public void setMinSdk(int minSdk) { 39 | this.minSdk = minSdk; 40 | } 41 | 42 | public void loadDexFile(@NotNull File dexFile) throws IOException { 43 | loadDexFile(DexBackedDexFile.fromInputStream(Opcodes.getDefault(), new BufferedInputStream(new FileInputStream(dexFile)))); 44 | } 45 | 46 | public void loadDexFile(@NotNull DexBackedDexFile dexFile) { 47 | for (DexBackedClassDef classDef : dexFile.getClasses()) { 48 | allClasses.put(classDef.getType(), classDef); 49 | } 50 | //jna这个类有不少本地方法,不会被混淆,所以通过它是否存在来判断是否使用了jna 51 | //没发现jna相关类时,每次加载外部dex时判断,如果已经找到则不用再判断 52 | if (!hasJna) { 53 | hasJna = allClasses.containsKey("Lcom/sun/jna/Native;"); 54 | } 55 | } 56 | 57 | public boolean hasJnaLib() { 58 | return hasJna; 59 | } 60 | 61 | // android6下直接通过jni调用jna方法会直接崩溃,所以需要判断是否有调用jna方法的指令.issue #31 62 | // 遍历方法字节码,判断是否有直接调用jna方法的指令,如果有返回true 63 | public boolean hasCallJnaMethod(@NotNull Method method) { 64 | if (!hasJnaLib()) { 65 | return false; 66 | } 67 | final MethodImplementation implementation = method.getImplementation(); 68 | if (implementation == null) { 69 | return false; 70 | } 71 | for (Instruction instruction : implementation.getInstructions()) { 72 | switch (instruction.getOpcode()) { 73 | //jna调用用的是调用接口方法,所以只需要检测这两个指令 74 | case INVOKE_INTERFACE, INVOKE_INTERFACE_RANGE -> { 75 | MethodReference methodReference = (MethodReference) ((ReferenceInstruction) instruction).getReference(); 76 | //如果调用的方法所在class实现接口中包含jna的Library则表示有直接调用jna的指令 77 | if (matchInterface(allClasses.get(methodReference.getDefiningClass()), "Lcom/sun/jna/Library;")) { 78 | return true; 79 | } 80 | } 81 | } 82 | } 83 | return false; 84 | } 85 | 86 | //判断class是否实现某个接口 87 | private boolean matchInterface(ClassDef classDef, String interfaceType) { 88 | if (classDef == null) { 89 | return false; 90 | } 91 | for (String defInterface : classDef.getInterfaces()) { 92 | if (defInterface.equals(interfaceType)) { 93 | return true; 94 | } 95 | //再查找接口 96 | final ClassDef ifClass = allClasses.get(defInterface); 97 | return matchInterface(ifClass, interfaceType); 98 | } 99 | return false; 100 | } 101 | 102 | //处理接口中静态域无法通过子类获得值问题 103 | public FieldReference getDirectFieldRef(@NotNull FieldReference reference) { 104 | final ClassDef classDef = allClasses.get(reference.getDefiningClass()); 105 | if (classDef == null) {//不再当前dex中或者在系统库 106 | return null; 107 | } 108 | 109 | final String fieldName = reference.getName(); 110 | final String fieldType = reference.getType(); 111 | 112 | final ClassDef newClassDef = findFieldDefiningClass(classDef, fieldName, fieldType); 113 | if (newClassDef != null) { 114 | return new ImmutableFieldReference(newClassDef.getType(), fieldName, fieldType); 115 | } 116 | 117 | 118 | return null; 119 | } 120 | 121 | private ClassDef findFieldDefiningClass(ClassDef classDef, String fieldName, String fieldType) { 122 | if (classDef == null) { 123 | return null; 124 | } 125 | //查找当前类的静态域,如果名称和类型匹配,则返回对应的classDef 126 | for (Field field : classDef.getStaticFields()) { 127 | if (field.getName().equals(fieldName) && field.getType().equals(fieldType)) { 128 | return classDef; 129 | } 130 | } 131 | //查找接口对应的classDef 132 | for (String defInterface : classDef.getInterfaces()) { 133 | final ClassDef definingClass = findFieldDefiningClass(allClasses.get(defInterface), fieldName, fieldType); 134 | if (definingClass != null) { 135 | return definingClass; 136 | } 137 | } 138 | return null; 139 | } 140 | 141 | @Nullable 142 | public MethodReference findDirectMethod(@NotNull MethodReference method) { 143 | if (minSdk < 23) { 144 | final ClassDef classDef = allClasses.get(method.getDefiningClass()); 145 | if (classDef == null) { 146 | return null; 147 | } 148 | //从父类的direct method中查找名称和签名相同的方法 149 | final ClassDef superClass = allClasses.get(classDef.getSuperclass()); 150 | return findDirectMethod(superClass, 151 | method.getName(), 152 | method.getParameterTypes(), 153 | method.getReturnType()); 154 | } 155 | return null; 156 | 157 | } 158 | 159 | @Nullable 160 | public ClassDef getClassDef(@NotNull String className) { 161 | return allClasses.get(className); 162 | } 163 | 164 | //先查找当前类的direct method,找不到则查找超类的direct method 165 | @Nullable 166 | private MethodReference findDirectMethod(@NotNull ClassDef thisClass, 167 | @NotNull String name, 168 | @NotNull List parameterTypes, 169 | @NotNull String returnType 170 | ) { 171 | for (ClassDef classDef = thisClass; classDef != null; classDef = allClasses.get(classDef.getSuperclass())) { 172 | for (Method directMethod : classDef.getDirectMethods()) { 173 | //方法名及方法签名完全相等 174 | if (directMethod.getName().equals(name) && 175 | directMethod.getParameterTypes().equals(parameterTypes) && 176 | directMethod.getReturnType().equals(returnType)) { 177 | return directMethod; 178 | } 179 | } 180 | } 181 | return null; 182 | } 183 | 184 | // 在当前类中查找方法,如果找不到则查找父类 185 | @Nullable 186 | public MethodReference findMethod(@NotNull ClassDef thisClass, 187 | @NotNull String name, 188 | @NotNull List parameterTypes, 189 | @NotNull String returnType 190 | ) { 191 | for (ClassDef classDef = thisClass; classDef != null; classDef = allClasses.get(classDef.getSuperclass())) { 192 | for (Method method : classDef.getMethods()) { 193 | //方法名及方法签名完全相等 194 | if (method.getName().equals(name) && 195 | method.getParameterTypes().equals(parameterTypes) && 196 | method.getReturnType().equals(returnType)) { 197 | return method; 198 | } 199 | } 200 | } 201 | return null; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/MyMethodUtil.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter; 2 | 3 | import com.android.tools.smali.dexlib2.AccessFlags; 4 | import com.android.tools.smali.dexlib2.iface.Method; 5 | import com.android.tools.smali.util.Hex; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.List; 9 | 10 | public class MyMethodUtil { 11 | public static boolean isConstructorOrAbstract(@NotNull Method method) { 12 | String name = method.getName(); 13 | if (name.equals("") || name.equals("")) {//构造方法或静态构造方法 14 | return true; 15 | } 16 | int accessFlags = method.getAccessFlags(); 17 | //本地方法或者抽象方法 18 | return AccessFlags.NATIVE.isSet(accessFlags) 19 | || AccessFlags.ABSTRACT.isSet(accessFlags); 20 | } 21 | 22 | public static boolean isBridgeOrSynthetic(@NotNull Method method) { 23 | int flags = method.getAccessFlags(); 24 | return AccessFlags.BRIDGE.isSet(flags) || 25 | AccessFlags.SYNTHETIC.isSet(flags); 26 | } 27 | 28 | @NotNull 29 | public static String getMethodSignature(@NotNull List parameterTypes, String returnType) { 30 | StringBuilder sig = new StringBuilder(); 31 | sig.append("("); 32 | for (CharSequence parameterType : parameterTypes) { 33 | sig.append(parameterType); 34 | } 35 | sig.append(")"); 36 | sig.append(returnType); 37 | return sig.toString(); 38 | } 39 | 40 | //jni函数命名规则 41 | @NotNull 42 | public static String getJniFunctionName(String className, String methodName, 43 | @NotNull List parameterTypes, String returnType) { 44 | 45 | StringBuilder funcName = new StringBuilder("Java_"); 46 | 47 | funcName.append(nameReplace(className).replace('/', '_')); 48 | 49 | funcName.append('_'); 50 | funcName.append(nameReplace(methodName)); 51 | if (!parameterTypes.isEmpty()) { 52 | funcName.append("__"); 53 | for (CharSequence parameterType : parameterTypes) { 54 | funcName.append(nameReplace(parameterType.toString()).replace('/', '_')); 55 | } 56 | } 57 | //todo 有的dex混淆后可能导致类名,方法名和参数完全一样,只有返回类型不一样, 58 | // 这样使用标准的jni命名规则导致同名函数产生,需要加上返回类型以区分函数,同时必须注册native方法 59 | if (returnType != null) { 60 | funcName.append('_').append(nameReplace(returnType).replace('/', '_')); 61 | } 62 | return funcName.toString(); 63 | } 64 | 65 | private static @NotNull String nameReplace(@NotNull String s) { 66 | int length = s.length(); 67 | StringBuilder sb = new StringBuilder(length * 6); 68 | for (int i = 0; i < length; i++) { 69 | char c = s.charAt(i); 70 | switch (c) { 71 | case '_' -> sb.append("_1"); 72 | case ';' -> sb.append("_2"); 73 | case '[' -> sb.append("_3"); 74 | case '$', '-', '+' -> { 75 | sb.append("_0"); 76 | sb.append(Hex.u2(c)); 77 | } 78 | default -> { 79 | if ( 80 | ((c & 0xFFFF) > 0x7F) 81 | ) { 82 | // 非ascii字符 83 | sb.append("_0"); 84 | sb.append(Hex.u2(c)); 85 | } else { 86 | sb.append(c); 87 | } 88 | } 89 | } 90 | } 91 | return sb.toString(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/instructionrewriter/NoneInstructionRewriter.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter.instructionrewriter; 2 | 3 | import com.android.tools.smali.dexlib2.Opcode; 4 | import com.android.tools.smali.dexlib2.Opcodes; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class NoneInstructionRewriter extends InstructionRewriter { 11 | public NoneInstructionRewriter() { 12 | //虚拟机使用39版本的opcode,所以这里需要使用同样版本 13 | super(Opcodes.forDexVersion(39)); 14 | } 15 | 16 | @Override 17 | public int replaceOpcode(Opcode opcode) { 18 | final Short value = opcodes.getOpcodeValue(opcode); 19 | if (value == null) { 20 | throw new RuntimeException("Invalid opcode " + opcode); 21 | } 22 | return value; 23 | } 24 | 25 | @NotNull 26 | @Override 27 | protected List getOpcodeList() { 28 | final List opcodeList = new ArrayList<>(); 29 | for (int i = 0; i < 256; i++) { 30 | opcodeList.add(opcodes.getOpcodeByValue(i)); 31 | } 32 | return opcodeList; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/instructionrewriter/RandomInstructionRewriter.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter.instructionrewriter; 2 | 3 | import com.android.tools.smali.dexlib2.Opcode; 4 | import com.android.tools.smali.dexlib2.Opcodes; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import java.util.ArrayList; 8 | import java.util.EnumMap; 9 | import java.util.List; 10 | import java.util.Random; 11 | 12 | public class RandomInstructionRewriter extends InstructionRewriter { 13 | private final List opcodeList = new ArrayList<>(256); 14 | private final EnumMap opcodeMap = new EnumMap<>(Opcode.class); 15 | 16 | public RandomInstructionRewriter() { 17 | //虚拟机使用39版本的opcode,所以这里需要使用同样版本 18 | super(Opcodes.forDexVersion(39)); 19 | final ArrayList randOpcodes = new ArrayList<>(); 20 | for (int i = 0; i < 256; i++) { 21 | final Opcode opcode = opcodes.getOpcodeByValue(i); 22 | opcodeList.add(opcode); 23 | if (opcode != null && opcode != Opcode.NOP) {//只处理dex指令,忽略odex 24 | randOpcodes.add(opcode); 25 | } 26 | } 27 | //随机opcode 28 | for (int i = 1; i < opcodeList.size(); i++) { 29 | final Opcode opcode = opcodeList.get(i); 30 | if (opcode != null) { 31 | final int randIdx = new Random().nextInt(randOpcodes.size()); 32 | final Opcode remove = randOpcodes.remove(randIdx); 33 | opcodeList.set(i, remove); 34 | } 35 | } 36 | 37 | 38 | for (int i = 0; i < opcodeList.size(); i++) { 39 | final Opcode opcode = opcodeList.get(i); 40 | if (opcode != null) opcodeMap.put(opcode, i); 41 | } 42 | 43 | } 44 | 45 | @Override 46 | public int replaceOpcode(Opcode opcode) { 47 | return opcodeMap.get(opcode); 48 | } 49 | 50 | @NotNull 51 | @Override 52 | protected List getOpcodeList() { 53 | return opcodeList; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/structs/EmptyAnnotationMethodParameter.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter.structs; 2 | 3 | import com.android.tools.smali.dexlib2.base.BaseMethodParameter; 4 | import com.android.tools.smali.dexlib2.iface.Annotation; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.util.Collections; 9 | import java.util.Set; 10 | 11 | public class EmptyAnnotationMethodParameter extends BaseMethodParameter { 12 | //不用记录参数名称 13 | @Nullable 14 | private final String name = null; 15 | @NotNull 16 | private final String type; 17 | 18 | public EmptyAnnotationMethodParameter(@NotNull String type) { 19 | this.type = type; 20 | } 21 | 22 | @NotNull 23 | @Override 24 | public Set getAnnotations() { 25 | //参数annotation总是为空 26 | return Collections.emptySet(); 27 | } 28 | 29 | @Nullable 30 | @Override 31 | public String getName() { 32 | return name; 33 | } 34 | 35 | @NotNull 36 | @Override 37 | public String getType() { 38 | return type; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/structs/EmptyConstructorMethod.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter.structs; 2 | 3 | import com.android.tools.smali.dexlib2.AccessFlags; 4 | import com.android.tools.smali.dexlib2.HiddenApiRestriction; 5 | import com.android.tools.smali.dexlib2.Opcode; 6 | import com.android.tools.smali.dexlib2.base.reference.BaseMethodReference; 7 | import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation; 8 | import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction10x; 9 | import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c; 10 | import com.android.tools.smali.dexlib2.iface.Annotation; 11 | import com.android.tools.smali.dexlib2.iface.Method; 12 | import com.android.tools.smali.dexlib2.iface.MethodImplementation; 13 | import com.android.tools.smali.dexlib2.iface.MethodParameter; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | import java.util.Collections; 17 | import java.util.List; 18 | import java.util.Set; 19 | 20 | //默认无参数构造方法 21 | 22 | /** 23 | * .method public constructor ()V 24 | * .locals 0 25 | * invoke-direct {p0}, Landroid/app/Application;->()V 26 | * return-void 27 | * .end method 28 | */ 29 | 30 | public class EmptyConstructorMethod extends BaseMethodReference implements Method { 31 | 32 | @NotNull 33 | private final String definingClass; 34 | 35 | @NotNull 36 | private final String superClass; 37 | 38 | public EmptyConstructorMethod(@NotNull String definingClass, @NotNull String superClass) { 39 | this.definingClass = definingClass; 40 | this.superClass = superClass; 41 | } 42 | 43 | @NotNull 44 | @Override 45 | public List getParameters() { 46 | return Collections.emptyList(); 47 | } 48 | 49 | @Override 50 | public int getAccessFlags() { 51 | return AccessFlags.CONSTRUCTOR.getValue() 52 | | AccessFlags.PUBLIC.getValue(); 53 | } 54 | 55 | @NotNull 56 | @Override 57 | public Set getAnnotations() { 58 | return Collections.emptySet(); 59 | } 60 | 61 | @NotNull 62 | @Override 63 | public Set getHiddenApiRestrictions() { 64 | return Collections.emptySet(); 65 | } 66 | 67 | @Override 68 | public MethodImplementation getImplementation() { 69 | final MutableMethodImplementation implementation = new MutableMethodImplementation(1); 70 | implementation.addInstruction(new BuilderInstruction35c(Opcode.INVOKE_DIRECT, 1, 71 | 0, 0, 0, 0, 0, new BaseMethodReference() { 72 | @NotNull 73 | @Override 74 | public String getDefiningClass() { 75 | return superClass; 76 | } 77 | 78 | @NotNull 79 | @Override 80 | public String getName() { 81 | return ""; 82 | } 83 | 84 | @NotNull 85 | @Override 86 | public List getParameterTypes() { 87 | return Collections.emptyList(); 88 | } 89 | 90 | @NotNull 91 | @Override 92 | public String getReturnType() { 93 | return "V"; 94 | } 95 | } 96 | )); 97 | implementation.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); 98 | return implementation; 99 | } 100 | 101 | @NotNull 102 | @Override 103 | public String getDefiningClass() { 104 | return definingClass; 105 | } 106 | 107 | @NotNull 108 | @Override 109 | public String getName() { 110 | return ""; 111 | } 112 | 113 | @NotNull 114 | @Override 115 | public List getParameterTypes() { 116 | return Collections.emptyList(); 117 | } 118 | 119 | @NotNull 120 | @Override 121 | public String getReturnType() { 122 | return "V"; 123 | } 124 | } -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/structs/LoadLibStaticBlockMethod.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter.structs; 2 | 3 | import com.android.tools.smali.dexlib2.AccessFlags; 4 | import com.android.tools.smali.dexlib2.HiddenApiRestriction; 5 | import com.android.tools.smali.dexlib2.Opcode; 6 | import com.android.tools.smali.dexlib2.base.reference.BaseMethodReference; 7 | import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation; 8 | import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction10x; 9 | import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c; 10 | import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c; 11 | import com.android.tools.smali.dexlib2.iface.Annotation; 12 | import com.android.tools.smali.dexlib2.iface.Method; 13 | import com.android.tools.smali.dexlib2.iface.MethodImplementation; 14 | import com.android.tools.smali.dexlib2.iface.MethodParameter; 15 | import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference; 16 | import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference; 17 | import org.jetbrains.annotations.NotNull; 18 | import org.jetbrains.annotations.Nullable; 19 | 20 | import java.util.Collections; 21 | import java.util.List; 22 | import java.util.Set; 23 | 24 | //静态初始化方法 25 | public class LoadLibStaticBlockMethod extends BaseMethodReference implements Method { 26 | 27 | private final Method method; 28 | 29 | @NotNull 30 | private final String definingClass; 31 | 32 | @NotNull 33 | private final String libName; 34 | 35 | 36 | public LoadLibStaticBlockMethod(@Nullable Method method, @NotNull String definingClass, @NotNull String libName) { 37 | this.method = method; 38 | this.definingClass = definingClass; 39 | this.libName = libName; 40 | } 41 | 42 | @NotNull 43 | @Override 44 | public String getDefiningClass() { 45 | return definingClass; 46 | } 47 | 48 | @NotNull 49 | @Override 50 | public String getName() { 51 | return ""; 52 | } 53 | 54 | @NotNull 55 | @Override 56 | public List getParameterTypes() { 57 | return Collections.emptyList(); 58 | } 59 | 60 | @NotNull 61 | @Override 62 | public List getParameters() { 63 | return Collections.emptyList(); 64 | } 65 | 66 | @NotNull 67 | @Override 68 | public String getReturnType() { 69 | return "V"; 70 | } 71 | 72 | 73 | @Override 74 | public int getAccessFlags() { 75 | return AccessFlags.CONSTRUCTOR.getValue() 76 | | AccessFlags.STATIC.getValue(); 77 | } 78 | 79 | @NotNull 80 | @Override 81 | public Set getAnnotations() { 82 | return Collections.emptySet(); 83 | } 84 | 85 | @NotNull 86 | @Override 87 | public Set getHiddenApiRestrictions() { 88 | return Collections.emptySet(); 89 | } 90 | 91 | @Override 92 | public MethodImplementation getImplementation() { 93 | final MutableMethodImplementation implementation; 94 | if (method != null && method.getImplementation() != null) { 95 | implementation = new MutableMethodImplementation(method.getImplementation()) { 96 | @Override 97 | public int getRegisterCount() {//起码需要一个寄存器 98 | return Math.max(1, super.getRegisterCount()); 99 | } 100 | }; 101 | injectCallLoadLibInsns(implementation); 102 | } else {//原来不存在,则需要添加返回指令 103 | implementation = new MutableMethodImplementation(1); 104 | injectCallLoadLibInsns(implementation); 105 | implementation.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); 106 | } 107 | return implementation; 108 | } 109 | 110 | private void injectCallLoadLibInsns(@NotNull MutableMethodImplementation implementation) { 111 | implementation.addInstruction(0, new BuilderInstruction21c(Opcode.CONST_STRING, 0, 112 | new ImmutableStringReference(libName))); 113 | implementation.addInstruction(1, new BuilderInstruction35c(Opcode.INVOKE_STATIC, 1, 114 | 0, 0, 0, 0, 0, 115 | new ImmutableMethodReference( 116 | "Ljava/lang/System;", 117 | "loadLibrary", 118 | Collections.singletonList("Ljava/lang/String;"), 119 | "V" 120 | ) 121 | )); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/structs/MyClassDef.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter.structs; 2 | 3 | import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference; 4 | import com.android.tools.smali.dexlib2.iface.Annotation; 5 | import com.android.tools.smali.dexlib2.iface.ClassDef; 6 | import com.android.tools.smali.dexlib2.iface.Field; 7 | import com.android.tools.smali.dexlib2.iface.Method; 8 | import com.google.common.collect.Iterators; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | import java.util.AbstractCollection; 13 | import java.util.Iterator; 14 | import java.util.List; 15 | import java.util.Set; 16 | 17 | 18 | public class MyClassDef extends BaseTypeReference implements ClassDef { 19 | @NotNull 20 | private final ClassDef classDef; 21 | @NotNull 22 | private final List directMethods; 23 | @NotNull 24 | private final List virtualMethods; 25 | 26 | 27 | public MyClassDef(@NotNull ClassDef classDef, 28 | @NotNull List directMethods, 29 | @NotNull List virtualMethods) { 30 | this.classDef = classDef; 31 | this.directMethods = directMethods; 32 | this.virtualMethods = virtualMethods; 33 | } 34 | 35 | @NotNull 36 | @Override 37 | public String getType() { 38 | return classDef.getType(); 39 | } 40 | 41 | @Override 42 | public int getAccessFlags() { 43 | return classDef.getAccessFlags(); 44 | } 45 | 46 | @Nullable 47 | @Override 48 | public String getSuperclass() { 49 | return classDef.getSuperclass(); 50 | } 51 | 52 | @NotNull 53 | @Override 54 | public List getInterfaces() { 55 | return classDef.getInterfaces(); 56 | } 57 | 58 | @Nullable 59 | @Override 60 | public String getSourceFile() { 61 | return null; 62 | } 63 | 64 | @NotNull 65 | @Override 66 | public Set getAnnotations() { 67 | return classDef.getAnnotations(); 68 | } 69 | 70 | @NotNull 71 | @Override 72 | public Iterable getStaticFields() { 73 | return classDef.getStaticFields(); 74 | } 75 | 76 | @NotNull 77 | @Override 78 | public Iterable getInstanceFields() { 79 | return classDef.getInstanceFields(); 80 | } 81 | 82 | @NotNull 83 | @Override 84 | public Iterable getFields() { 85 | return classDef.getFields(); 86 | } 87 | 88 | @NotNull 89 | @Override 90 | public Iterable getDirectMethods() { 91 | return directMethods; 92 | } 93 | 94 | @NotNull 95 | @Override 96 | public Iterable getVirtualMethods() { 97 | return virtualMethods; 98 | } 99 | 100 | @NotNull 101 | @Override 102 | public Iterable getMethods() { 103 | // return Iterables.concat(directMethods, virtualMethods); 104 | return new AbstractCollection() { 105 | @NotNull 106 | @Override 107 | public Iterator iterator() { 108 | return Iterators.concat(directMethods.iterator(), virtualMethods.iterator()); 109 | } 110 | 111 | @Override 112 | public int size() { 113 | return directMethods.size() + virtualMethods.size(); 114 | } 115 | }; 116 | } 117 | } -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/structs/RegisterNativesUtilClassDef.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter.structs; 2 | 3 | import com.android.tools.smali.dexlib2.AccessFlags; 4 | import com.android.tools.smali.dexlib2.HiddenApiRestriction; 5 | import com.android.tools.smali.dexlib2.base.reference.BaseMethodReference; 6 | import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference; 7 | import com.android.tools.smali.dexlib2.iface.*; 8 | import org.jetbrains.annotations.Contract; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | import org.jetbrains.annotations.Unmodifiable; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collections; 15 | import java.util.List; 16 | import java.util.Set; 17 | 18 | /** 19 | * 每个类调用的静态初始化方法,一般情况一个classes.dex对应一个注册方法 20 | * 需要把它放在主classes.dex里 21 | *

22 | * 静态初始化方法里增加加载本地库代码 23 | */ 24 | public class RegisterNativesUtilClassDef extends BaseTypeReference implements ClassDef { 25 | @NotNull 26 | private final String type; 27 | @NotNull 28 | private final List nativeMethodNames; 29 | 30 | private final String libName; 31 | 32 | public RegisterNativesUtilClassDef(@NotNull String type, 33 | @NotNull List nativeMethodNames, 34 | @NotNull String libName) { 35 | this.type = type; 36 | this.nativeMethodNames = nativeMethodNames; 37 | this.libName = libName; 38 | } 39 | 40 | @NotNull 41 | @Override 42 | public String getType() { 43 | return type; 44 | } 45 | 46 | 47 | @Override 48 | public int getAccessFlags() { 49 | return AccessFlags.PUBLIC.getValue(); 50 | } 51 | 52 | @Nullable 53 | @Override 54 | public String getSuperclass() { 55 | return "Ljava/lang/Object;"; 56 | } 57 | 58 | @NotNull 59 | @Override 60 | public List getInterfaces() { 61 | return Collections.emptyList(); 62 | } 63 | 64 | @Nullable 65 | @Override 66 | public String getSourceFile() { 67 | return null; 68 | } 69 | 70 | @NotNull 71 | @Override 72 | public Set getAnnotations() { 73 | return Collections.emptySet(); 74 | } 75 | 76 | @NotNull 77 | @Override 78 | public Iterable getStaticFields() { 79 | return Collections.emptyList(); 80 | } 81 | 82 | @NotNull 83 | @Override 84 | public Iterable getInstanceFields() { 85 | return Collections.emptyList(); 86 | } 87 | 88 | @NotNull 89 | @Override 90 | public Iterable getFields() { 91 | return Collections.emptyList(); 92 | } 93 | 94 | @NotNull 95 | @Override 96 | public Iterable getDirectMethods() { 97 | final ArrayList methods = new ArrayList<>(); 98 | //静态初始化方法 99 | methods.add(new LoadLibStaticBlockMethod(null, type, libName)); 100 | //空构造函数 101 | methods.add(new EmptyConstructorMethod(type, "Ljava/lang/Object;")); 102 | for (String methodName : nativeMethodNames) { 103 | methods.add(new NativeMethod(type, methodName)); 104 | } 105 | return methods; 106 | } 107 | 108 | @NotNull 109 | @Override 110 | public Iterable getVirtualMethods() { 111 | return Collections.emptyList(); 112 | } 113 | 114 | @NotNull 115 | @Override 116 | public Iterable getMethods() { 117 | //virtualMethods为空,总方法只需要返回directMethods就行 118 | return getDirectMethods(); 119 | } 120 | 121 | private static class NativeMethod extends BaseMethodReference implements Method { 122 | 123 | @NotNull 124 | private final String type; 125 | 126 | private final String methodName; 127 | 128 | public NativeMethod(@NotNull String type, String methodName) { 129 | this.type = type; 130 | this.methodName = methodName; 131 | } 132 | 133 | @Contract(pure = true) 134 | @NotNull 135 | @Override 136 | public @Unmodifiable List getParameters() { 137 | return Collections.emptyList(); 138 | } 139 | 140 | @Override 141 | public int getAccessFlags() { 142 | return AccessFlags.NATIVE.getValue() 143 | | AccessFlags.STATIC.getValue() 144 | | AccessFlags.PUBLIC.getValue(); 145 | } 146 | 147 | @Contract(pure = true) 148 | @NotNull 149 | @Override 150 | public @Unmodifiable Set getAnnotations() { 151 | return Collections.emptySet(); 152 | } 153 | 154 | @Contract(pure = true) 155 | @NotNull 156 | @Override 157 | public @Unmodifiable Set getHiddenApiRestrictions() { 158 | return Collections.emptySet(); 159 | } 160 | 161 | @Contract(pure = true) 162 | @Override 163 | public @Nullable MethodImplementation getImplementation() { 164 | return null; 165 | } 166 | 167 | @NotNull 168 | @Override 169 | public String getDefiningClass() { 170 | return type; 171 | } 172 | 173 | @NotNull 174 | @Override 175 | public String getName() { 176 | return methodName; 177 | } 178 | 179 | @Contract(value = " -> new", pure = true) 180 | @NotNull 181 | @Override 182 | public @Unmodifiable List getParameterTypes() { 183 | return Collections.singletonList("I"); 184 | } 185 | 186 | @NotNull 187 | @Override 188 | public String getReturnType() { 189 | return "V"; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/testbuild/ClassMethodImplCollection.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter.testbuild; 2 | 3 | import com.android.tools.smali.dexlib2.iface.*; 4 | import com.android.tools.smali.dexlib2.util.MethodUtil; 5 | import com.nmmedit.apkprotect.dex2c.converter.MyMethodUtil; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | /** 14 | * 收集方法具体代码,把它放入全新的dex中 15 | * 当前用于测试,实际使用需要把它转换为c代码, 16 | */ 17 | public class ClassMethodImplCollection implements ClassDef { 18 | private final ClassDef classDef; 19 | 20 | private final StringBuilder codeContent; 21 | 22 | public ClassMethodImplCollection(ClassDef classDef, StringBuilder sb) { 23 | this.classDef = classDef; 24 | this.codeContent = sb; 25 | } 26 | 27 | @NotNull 28 | @Override 29 | public String getType() { 30 | return classDef.getType(); 31 | } 32 | 33 | @Override 34 | public int compareTo(@NotNull CharSequence o) { 35 | return classDef.compareTo(o); 36 | } 37 | 38 | @Override 39 | public int getAccessFlags() { 40 | return classDef.getAccessFlags(); 41 | } 42 | 43 | @Nullable 44 | @Override 45 | public String getSuperclass() { 46 | return classDef.getSuperclass(); 47 | } 48 | 49 | @NotNull 50 | @Override 51 | public List getInterfaces() { 52 | return classDef.getInterfaces(); 53 | } 54 | 55 | @Nullable 56 | @Override 57 | public String getSourceFile() { 58 | //忽略 59 | return null; 60 | } 61 | 62 | @NotNull 63 | @Override 64 | public Set getAnnotations() { 65 | return classDef.getAnnotations(); 66 | } 67 | 68 | @NotNull 69 | @Override 70 | public Iterable getStaticFields() { 71 | return classDef.getStaticFields(); 72 | } 73 | 74 | @NotNull 75 | @Override 76 | public Iterable getInstanceFields() { 77 | return classDef.getInstanceFields(); 78 | } 79 | 80 | @NotNull 81 | @Override 82 | public Iterable getFields() { 83 | return classDef.getFields(); 84 | } 85 | 86 | @NotNull 87 | @Override 88 | public Iterable getDirectMethods() { 89 | Iterable directMethods = classDef.getDirectMethods(); 90 | return convertMethods(directMethods); 91 | } 92 | 93 | @NotNull 94 | @Override 95 | public Iterable getVirtualMethods() { 96 | Iterable virtualMethods = classDef.getVirtualMethods(); 97 | return convertMethods(virtualMethods); 98 | } 99 | 100 | @NotNull 101 | @Override 102 | public Iterable getMethods() { 103 | Iterable methods = classDef.getMethods(); 104 | return convertMethods(methods); 105 | } 106 | 107 | private @NotNull Iterable convertMethods(@NotNull Iterable methods) { 108 | ArrayList newMethods = new ArrayList<>(); 109 | for (Method method : methods) { 110 | if (MyMethodUtil.isConstructorOrAbstract(method)) { 111 | continue; 112 | } 113 | newMethods.add(method); 114 | methodToC(method); 115 | } 116 | return newMethods; 117 | } 118 | 119 | private void methodToC(@NotNull Method method) { 120 | MethodImplementation implementation = method.getImplementation(); 121 | if (implementation == null) { 122 | return; 123 | } 124 | int registerCount = implementation.getRegisterCount(); 125 | int parameterRegisterCount = MethodUtil.getParameterRegisterCount(method); 126 | List parameterTypes = method.getParameterTypes(); 127 | 128 | String code = JniTemp.genJniCode(getType(), method.getName(), parameterTypes, MethodUtil.isStatic(method), registerCount, parameterRegisterCount, method.getReturnType()); 129 | codeContent.append(code); 130 | codeContent.append('\n'); 131 | 132 | } 133 | 134 | @Override 135 | public int length() { 136 | return classDef.length(); 137 | } 138 | 139 | @Override 140 | public char charAt(int index) { 141 | return classDef.charAt(index); 142 | } 143 | 144 | @Override 145 | public @NotNull CharSequence subSequence(int start, int end) { 146 | return classDef.subSequence(start, end); 147 | } 148 | 149 | @Override 150 | public void validateReference() throws InvalidReferenceException { 151 | classDef.validateReference(); 152 | } 153 | 154 | @NotNull 155 | @Override 156 | public String toString() { 157 | return classDef.toString(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/converter/testbuild/JniTemp.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.converter.testbuild; 2 | 3 | import org.jetbrains.annotations.Contract; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * 生成开发测试用的jni代码,依赖libdex从符号dex里提取各种信息 10 | */ 11 | public class JniTemp { 12 | public static final String CODE_TEMP_INSNS_TRIES = "\n\n const DexCode *dexCode = findDexCode(\"%s\", \"%s\");\n" + 13 | " const u2 *insns = dexCode->insns;\n" + 14 | " u4 insnsSize = dexCode->insnsSize;\n" + 15 | "\n" + 16 | " size_t size = dexGetTryHandlerSize(dexCode);\n" + 17 | " const DexTry *tries = dexGetTries(dexCode);\n" + 18 | "\n" + 19 | " u1 *tryHandler = (u1 *) malloc(size + 4);\n" + 20 | " u2 *ts = (u2 *) tryHandler;\n" + 21 | " ts[0] = dexCode->triesSize;\n" + 22 | " ts[1] = 0;\n" + 23 | "\n" + 24 | " memcpy(tryHandler + 4, tries, size);\n" + 25 | "\n" + 26 | " jvalue value = dvmInterpret(env, insns, insnsSize, regs, (const u1 *) tryHandler, &dvmResolver);\n" + 27 | " free(tryHandler);\n" + 28 | "\n"; 29 | 30 | public static @NotNull String genJniCode(@NotNull String classType, String methodName, List parameterTypes, boolean isStatic, int registerCount, int parameterRegisterCount, String returnType) { 31 | StringBuilder params = new StringBuilder(); 32 | 33 | //寄存器初始化 34 | StringBuilder regsAss = new StringBuilder(String.format(" u8 regs[%d];\n memset(regs, 0, sizeof(regs));\n", registerCount)); 35 | 36 | String clazzName = classType.substring(1, classType.length() - 1); 37 | String jniCode = String.format("JNIEXPORT %s Java_%s_%s(JNIEnv *env, %s ", 38 | getJNIType(returnType), 39 | clazzName.replace('/', '_'), 40 | methodName, 41 | isStatic ? "jclass jcls" : "jobject thiz" 42 | ); 43 | 44 | int paramRegStart = registerCount - parameterRegisterCount; 45 | if (!isStatic) { 46 | regsAss.append(String.format(" regs[%d] = (u8) thiz;\n", paramRegStart++)); 47 | } 48 | 49 | int size = parameterTypes.size(); 50 | for (int i = 0; i < size; i++) { 51 | String type = parameterTypes.get(i).toString(); 52 | String jniType = getJNIType(type); 53 | int argNum = isStatic ? i : i + 1; 54 | params.append(jniType) 55 | .append(" p") 56 | .append(argNum); 57 | if (type.startsWith("[") || type.startsWith("L")) {//对象类型,转为u8 58 | regsAss.append(String.format(" regs[%d] = (u8) p%d;\n", paramRegStart++, argNum)); 59 | } else if (type.equals("F")) {//先转为u4指针,然后再取值,把float存进4字节里,double同理 60 | regsAss.append(String.format(" regs[%d] = *(u4 *) &p%d;\n", paramRegStart++, argNum)); 61 | } else if (type.equals("D")) { 62 | regsAss.append(String.format(" regs[%d] = *(u8 *) &p%d;\n", paramRegStart++, argNum)); 63 | } else { 64 | regsAss.append(String.format(" regs[%d] = p%d;\n", paramRegStart++, argNum)); 65 | } 66 | if (type.equals("J") || type.equals("D")) {//long和double需要两个寄存器 67 | paramRegStart++; 68 | } 69 | if (i < size - 1) {//最后不用加, 70 | params.append(", "); 71 | } 72 | 73 | } 74 | if (!params.isEmpty()) { 75 | jniCode += ", " + params.toString(); 76 | } 77 | jniCode += ") {\n"; 78 | jniCode += regsAss; 79 | jniCode += String.format(CODE_TEMP_INSNS_TRIES, classType, methodName); 80 | 81 | 82 | if (!returnType.equals("V")) { 83 | char typeCh = returnType.charAt(0); 84 | jniCode += String.format(" return value.%s;\n", Character.toLowerCase(typeCh == '[' ? 'L' : typeCh)); 85 | } 86 | jniCode += "}\n"; 87 | 88 | return jniCode; 89 | } 90 | 91 | @Contract(pure = true) 92 | public static String getJNIType(@NotNull String type) { 93 | return switch (type) { 94 | case "Z" -> "jboolean"; 95 | case "B" -> "jbyte"; 96 | case "S" -> "jshort"; 97 | case "C" -> "jchar"; 98 | case "I" -> "jint"; 99 | case "F" -> "jfloat"; 100 | case "J" -> "jlong"; 101 | case "D" -> "jdouble"; 102 | case "Ljava/lang/String;" -> "jstring"; 103 | case "V" -> "void"; 104 | default -> "jobject"; 105 | }; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/filters/BasicKeepConfig.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.filters; 2 | 3 | import com.android.tools.smali.dexlib2.iface.ClassDef; 4 | import com.android.tools.smali.dexlib2.iface.Method; 5 | import com.nmmedit.apkprotect.dex2c.converter.MyMethodUtil; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | /** 9 | * 基本过滤规则,其他规则必须先通过它, 然后才能处理自定义规则 10 | */ 11 | 12 | public class BasicKeepConfig implements ClassAndMethodFilter { 13 | @Override 14 | public boolean acceptClass(@NotNull ClassDef classDef) { 15 | return !classDef.getType().startsWith("Landroidx/core/app/CoreComponentFactory") 16 | && !classDef.getType().startsWith("Landroid/support/v4/app/CoreComponentFactory"); 17 | } 18 | 19 | @Override 20 | public boolean acceptMethod(Method method) { 21 | return !MyMethodUtil.isConstructorOrAbstract(method) && !MyMethodUtil.isBridgeOrSynthetic(method); 22 | } 23 | } -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/filters/ClassAndMethodFilter.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.filters; 2 | 3 | import com.android.tools.smali.dexlib2.iface.ClassDef; 4 | import com.android.tools.smali.dexlib2.iface.Method; 5 | 6 | public interface ClassAndMethodFilter { 7 | boolean acceptClass(ClassDef classDef); 8 | 9 | boolean acceptMethod(Method method); 10 | } 11 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/filters/SimpleConvertConfig.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.filters; 2 | 3 | import com.android.tools.smali.dexlib2.iface.ClassDef; 4 | import com.android.tools.smali.dexlib2.iface.Method; 5 | 6 | public class SimpleConvertConfig implements ClassAndMethodFilter { 7 | private final ClassAndMethodFilter filter; 8 | private final SimpleRules simpleRule; 9 | 10 | public SimpleConvertConfig(ClassAndMethodFilter filter, SimpleRules simpleRules) { 11 | this.filter = filter; 12 | this.simpleRule = simpleRules; 13 | } 14 | 15 | @Override 16 | public boolean acceptClass(ClassDef classDef) { 17 | if (filter != null && !filter.acceptClass(classDef)) { 18 | return false; 19 | } 20 | return simpleRule != null && simpleRule.matchClass( 21 | classDef.getType(), 22 | classDef.getSuperclass(), 23 | classDef.getInterfaces()); 24 | } 25 | 26 | @Override 27 | public boolean acceptMethod(Method method) { 28 | if (filter != null && !filter.acceptMethod(method)) { 29 | return false; 30 | } 31 | return simpleRule != null && simpleRule.matchMethod(method.getName()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/dex2c/filters/SimpleRules.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.dex2c.filters; 2 | 3 | import com.google.common.collect.HashMultimap; 4 | import org.jetbrains.annotations.Contract; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.IOException; 10 | import java.io.Reader; 11 | import java.rmi.RemoteException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.Set; 15 | 16 | /** 17 | * class * extends android.app.Activity 18 | * class * implements java.io.Serializable 19 | * class my.package.AClass 20 | * class my.package.* { *; } 21 | * class * extends java.util.ArrayList { 22 | * if*; 23 | * } 24 | * class A { 25 | * } 26 | * class B extends A { 27 | * } 28 | * class C extends B { 29 | * } 30 | * The rule 'class * extends A' only match B 31 | */ 32 | public class SimpleRules { 33 | private final HashMultimap convertRules = HashMultimap.create(); 34 | private Set methodRules; 35 | 36 | public SimpleRules() { 37 | } 38 | 39 | @Contract(pure = true) 40 | private static @NotNull String classNameToType(@NotNull String className) { 41 | return "L" + className.replace('.', '/') + ";"; 42 | } 43 | 44 | @NotNull 45 | private static String toRegex(@NotNull String s) { 46 | final StringBuilder sb = new StringBuilder(s.length() + 3); 47 | for (int i = 0; i < s.length(); i++) { 48 | final char c = s.charAt(i); 49 | if (c == '*') { 50 | sb.append('.'); 51 | } 52 | sb.append(c); 53 | } 54 | return sb.toString(); 55 | } 56 | 57 | public void parse(Reader ruleReader) throws IOException { 58 | try (BufferedReader reader = new BufferedReader(ruleReader)) { 59 | ClassRule classRule = null; 60 | final ArrayList methodNameList = new ArrayList<>(); 61 | boolean methodParsing = false; 62 | int lineNumb = 0; 63 | String line; 64 | while ((line = reader.readLine()) != null) { 65 | line = line.trim(); 66 | if (line.isEmpty()) {//empty line 67 | lineNumb++; 68 | continue; 69 | } 70 | if (line.startsWith("class")) { 71 | final String[] split = line.split(" +"); 72 | final int length = split.length; 73 | if (length < 2) { 74 | throw new RemoteException("Error rule " + lineNumb + ": " + line); 75 | } 76 | String className = split[1]; 77 | String supperName = ""; 78 | String interfaceName = ""; 79 | if (length >= 4) { 80 | if ("extends".equals(split[2])) {//class * extends A 81 | supperName = split[3]; 82 | } else if ("implements".equals(split[2])) {//class * implements I 83 | interfaceName = split[3]; 84 | } 85 | } 86 | classRule = new ClassRule(className, supperName, interfaceName); 87 | int mstart; 88 | if ((mstart = line.indexOf('{')) != -1) { // my.pkg.A { methodA;methodB;} 89 | int mend; 90 | if ((mend = line.indexOf('}')) != -1) { 91 | final String[] methodNames = line.substring(mstart + 1, mend).trim().split(";"); 92 | if (methodNames.length == 0) { 93 | throw new RemoteException("Error rule " + lineNumb + ": " + line); 94 | } 95 | for (String name : methodNames) { 96 | convertRules.put(classRule, new MethodRule(name)); 97 | } 98 | } else { 99 | methodNameList.clear(); 100 | methodParsing = true; 101 | } 102 | } else { 103 | //any methods 104 | convertRules.put(classRule, new MethodRule("*")); 105 | } 106 | } else if (methodParsing) { 107 | // my.pkg.A { 108 | // methodA; 109 | // methodB; 110 | // } 111 | if (line.indexOf('}') != -1) { 112 | if (methodNameList.isEmpty()) { 113 | throw new RemoteException("Error rule " + lineNumb + ": " + line); 114 | } 115 | for (String methodName : methodNameList) { 116 | if ("".equals(methodName)) { 117 | continue; 118 | } 119 | convertRules.put(classRule, new MethodRule(methodName)); 120 | } 121 | methodParsing = false; 122 | } else { 123 | methodNameList.add(line.replace(";", "")); 124 | } 125 | } else { 126 | throw new RemoteException("Error rule " + lineNumb + ": " + line); 127 | } 128 | 129 | lineNumb++; 130 | } 131 | } 132 | } 133 | 134 | public boolean matchClass(@NotNull String classType, @Nullable String supperType, @NotNull List ifacTypes) { 135 | for (ClassRule rule : convertRules.keySet()) { 136 | final String typeRegex = toRegex(classNameToType(rule.className)); 137 | if (classType.matches(typeRegex)) {// match classType 138 | if (!"".equals(rule.supperName)) {//supper name not empty 139 | if (supperType != null) { 140 | final String type = classNameToType(rule.supperName); 141 | if (supperType.equals(type)) { 142 | methodRules = convertRules.get(rule); 143 | return true; 144 | } 145 | } 146 | continue; 147 | } 148 | if (!"".equals(rule.interfaceName)) {//interface name not empty 149 | for (String iface : ifacTypes) { 150 | if (iface.equals(classNameToType(rule.interfaceName))) { 151 | methodRules = convertRules.get(rule); 152 | return true; 153 | } 154 | } 155 | continue; 156 | } 157 | methodRules = convertRules.get(rule); 158 | return true; 159 | } 160 | } 161 | methodRules = null; 162 | return false; 163 | } 164 | 165 | public boolean matchMethod(String methodName) { 166 | if (methodRules == null || methodName == null) { 167 | return false; 168 | } 169 | for (MethodRule methodRule : methodRules) { 170 | if (methodName.matches(toRegex(methodRule.methodName))) { 171 | return true; 172 | } 173 | } 174 | return false; 175 | } 176 | 177 | private static class ClassRule { 178 | @NotNull 179 | private final String className; 180 | //supper class 181 | @NotNull 182 | private final String supperName; 183 | //interface 184 | @NotNull 185 | private final String interfaceName; 186 | 187 | public ClassRule(@NotNull String className) { 188 | this(className, "", ""); 189 | } 190 | 191 | public ClassRule(@NotNull String className, @NotNull String supperName, @NotNull String interfaceName) { 192 | this.className = className; 193 | this.supperName = supperName; 194 | this.interfaceName = interfaceName; 195 | } 196 | 197 | @Override 198 | public boolean equals(Object o) { 199 | if (this == o) return true; 200 | if (o == null || getClass() != o.getClass()) return false; 201 | 202 | ClassRule classRule = (ClassRule) o; 203 | 204 | if (!className.equals(classRule.className)) return false; 205 | if (!supperName.equals(classRule.supperName)) return false; 206 | return interfaceName.equals(classRule.interfaceName); 207 | } 208 | 209 | @Override 210 | public int hashCode() { 211 | int result = className.hashCode(); 212 | result = 31 * result + supperName.hashCode(); 213 | result = 31 * result + interfaceName.hashCode(); 214 | return result; 215 | } 216 | } 217 | 218 | private static class MethodRule { 219 | @NotNull 220 | private final String methodName; 221 | // args ? 222 | // private final List args; 223 | 224 | public MethodRule(@NotNull String methodName) { 225 | this.methodName = methodName; 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/log/VmpLogger.kt: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.log 2 | 3 | interface VmpLogger { 4 | fun info(msg: String?) 5 | fun error(msg: String?) 6 | fun warning(msg: String?) 7 | } 8 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/sign/ApkVerifyCodeGenerator.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.sign; 2 | 3 | /** 4 | * 验证公钥,通过公钥生成c代码, 运行时读取apk签名块里公钥, 进行对比从而验证apk是否被重新签名 5 | */ 6 | public class ApkVerifyCodeGenerator { 7 | private final String keyStorePath; 8 | private final String alias; 9 | private final String keyStorePassword; 10 | 11 | public ApkVerifyCodeGenerator(String keyStorePath, String alias, String keyStorePassword) { 12 | this.keyStorePath = keyStorePath; 13 | this.alias = alias; 14 | this.keyStorePassword = keyStorePassword; 15 | } 16 | 17 | public String generate() { 18 | final byte[] publicKey = PublicKeyUtils.getPublicKey(keyStorePath, alias, keyStorePassword); 19 | if (publicKey == null) { 20 | throw new RuntimeException("publicKey == null"); 21 | } 22 | final StringBuilder sb = new StringBuilder(); 23 | for (int i = 0; i < publicKey.length; i++) { 24 | if (i % 10 == 0) { 25 | sb.append(" \\\\\n "); 26 | } 27 | sb.append(String.format("0x%02x, ", publicKey[i] & 0xFF)); 28 | } 29 | return sb.toString(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/sign/PublicKeyUtils.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.sign; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import java.io.FileInputStream; 6 | import java.security.KeyStore; 7 | import java.security.PublicKey; 8 | import java.security.cert.Certificate; 9 | import java.util.Enumeration; 10 | 11 | public class PublicKeyUtils { 12 | public static byte @Nullable [] getPublicKey(String keyStorePath, String alias, String password) { 13 | try { 14 | final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); 15 | ks.load(new FileInputStream(keyStorePath), password.toCharArray()); 16 | if (alias == null || alias.isEmpty()) { 17 | final Enumeration aliases = ks.aliases(); 18 | if (aliases == null) { 19 | return null; 20 | } 21 | if (aliases.hasMoreElements()) { 22 | alias = aliases.nextElement();//取第一个证书别名 23 | } 24 | } 25 | final Certificate[] chain = ks.getCertificateChain(alias); 26 | if (chain == null || chain.length == 0) { 27 | throw new RuntimeException( 28 | keyStorePath + " entry \"" + alias + "\" does not contain certificates"); 29 | } 30 | 31 | final PublicKey publicKey = chain[0].getPublicKey(); 32 | return publicKey.getEncoded(); 33 | } catch (Exception e) { 34 | throw new RuntimeException(e); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/util/CmakeUtils.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.util; 2 | 3 | import com.nmmedit.apkprotect.data.Prefs; 4 | import com.nmmedit.apkprotect.dex2c.converter.instructionrewriter.InstructionRewriter; 5 | import com.nmmedit.apkprotect.sign.ApkVerifyCodeGenerator; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.io.*; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.Random; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | import java.util.stream.Collectors; 16 | 17 | public class CmakeUtils { 18 | 19 | //根据指令重写规则,重新生成新的opcode 20 | public static void writeOpcodeHeaderFile(File source, @NotNull InstructionRewriter instructionRewriter) throws IOException { 21 | final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( 22 | new FileInputStream(source), StandardCharsets.UTF_8)); 23 | 24 | final String collect = bufferedReader.lines().collect(Collectors.joining("\n")); 25 | final Pattern opcodePattern = Pattern.compile( 26 | "enum Opcode \\{.*?};", 27 | Pattern.MULTILINE | Pattern.DOTALL); 28 | final StringWriter opcodeContent = new StringWriter(); 29 | final StringWriter gotoTableContent = new StringWriter(); 30 | instructionRewriter.generateConfig(opcodeContent, gotoTableContent); 31 | String headerContent = opcodePattern 32 | .matcher(collect) 33 | .replaceAll(String.format("enum Opcode {\n%s};\n", opcodeContent.toString())); 34 | 35 | //根据opcode生成goto表 36 | final Pattern patternGotoTable = Pattern.compile( 37 | "_name\\[kNumPackedOpcodes] = \\{.*?};", 38 | Pattern.MULTILINE | Pattern.DOTALL); 39 | headerContent = patternGotoTable 40 | .matcher(headerContent) 41 | .replaceAll(String.format("_name[kNumPackedOpcodes] = { \\\\\n%s};\n", gotoTableContent)); 42 | 43 | try (FileWriter fileWriter = new FileWriter(source)) { 44 | fileWriter.write(headerContent); 45 | } 46 | } 47 | 48 | //读取证书信息,并把公钥写入签名验证文件里,运行时对apk进行签名校验 49 | private static void writeApkVerifierFile(String packageName, File source, ApkVerifyCodeGenerator apkVerifyCodeGenerator) throws IOException { 50 | if (apkVerifyCodeGenerator == null) { 51 | return; 52 | } 53 | final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( 54 | new FileInputStream(source), StandardCharsets.UTF_8)); 55 | 56 | final String lines = bufferedReader.lines().collect(Collectors.joining("\n")); 57 | String dataPlaceHolder = "#define publicKeyPlaceHolder"; 58 | 59 | String content = lines.replaceAll(dataPlaceHolder, dataPlaceHolder + apkVerifyCodeGenerator.generate()); 60 | content = content.replaceAll("(#define PACKAGE_NAME) .*\n", "$1 \"" + packageName + "\"\n"); 61 | 62 | try (FileWriter fileWriter = new FileWriter(source)) { 63 | fileWriter.write(content); 64 | } 65 | } 66 | 67 | public static void writeCmakeFile(File cmakeTemp, String libNmmpName, String libVmName, String cxxFlags) throws IOException { 68 | final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( 69 | new FileInputStream(cmakeTemp), StandardCharsets.UTF_8)); 70 | 71 | String lines = bufferedReader.lines().collect(Collectors.joining("\n")); 72 | //定位cmake里的语句,防止替换错误 73 | String libNameFormat = "set\\(LIBNAME_PLACEHOLDER \"%s\"\\)"; 74 | 75 | //替换原本libname 76 | lines = lines.replaceAll(String.format(libNameFormat, "nmmp"), String.format(libNameFormat, libNmmpName)); 77 | 78 | libNameFormat = "set\\(LIBNMMVM_NAME \"%s\" CACHE INTERNAL \"lib %s name\"\\)"; 79 | lines = lines.replaceAll(String.format(libNameFormat, "nmmvm", "nmmvm"), String.format(libNameFormat, libVmName, libVmName)); 80 | 81 | //额外FLAGS 82 | lines = lines.replaceAll("-fvisibility=hidden", "-fvisibility=hidden " + cxxFlags); 83 | 84 | FileHelper.writeToFile(cmakeTemp, lines); 85 | } 86 | 87 | 88 | public static void generateCSources(File srcDir, InstructionRewriter instructionRewriter) throws IOException { 89 | final File vmsrcFile = new File(FileUtils.getHomePath(), "tools/vmsrc.zip"); 90 | if (!vmsrcFile.exists()) { 91 | //警告:如果外部源码存在不会复制内部vmsrc.zip出去,需要删除外部源码文件才能保证vmsrc.zip正确更新 92 | vmsrcFile.getParentFile().mkdirs(); 93 | //copy vmsrc.zip to external directory 94 | try ( 95 | InputStream inputStream = CmakeUtils.class.getResourceAsStream("/vmsrc.zip"); 96 | final FileOutputStream outputStream = new FileOutputStream(vmsrcFile); 97 | ) { 98 | FileHelper.copyStream(inputStream, outputStream); 99 | } 100 | } 101 | final List cSources = ApkUtils.extractFiles(vmsrcFile, ".*", srcDir); 102 | 103 | //处理指令及apk验证,生成新的c文件 104 | for (File source : cSources) { 105 | if (source.getName().endsWith("DexOpcodes.h")) { 106 | //根据指令重写规则重新生成DexOpcodes.h文件 107 | writeOpcodeHeaderFile(source, instructionRewriter); 108 | } else if (source.getName().equals("CMakeLists.txt")) { 109 | //处理cmake里配置的本地库名 110 | writeCmakeFile(source, Prefs.getNmmpName(), Prefs.getVmName(), Prefs.getCxxFlags()); 111 | } else if (source.getName().endsWith("vm.h")) { 112 | writeRandomResolver(source); 113 | } else if (source.getName().endsWith("JNIWrapper.h")) { 114 | writeRandomJNIWrapper(source); 115 | } 116 | } 117 | } 118 | 119 | private static void writeRandomResolver(File source) throws IOException { 120 | final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( 121 | new FileInputStream(source), StandardCharsets.UTF_8)); 122 | String lines = bufferedReader.lines().collect(Collectors.joining("\n")); 123 | final Pattern p = Pattern.compile("typedef struct \\{([^}]*?)} vmResolver;", Pattern.MULTILINE | Pattern.DOTALL); 124 | final Matcher matcherResolver = p.matcher(lines); 125 | if (matcherResolver.find()) { 126 | final String body = matcherResolver.group(1); 127 | //match function pointer 128 | final Pattern funcPattern = Pattern.compile("([^();]* \\**\\(\\*[a-zA-z0-9]*\\)\\([^();]*\\);)", Pattern.MULTILINE | Pattern.DOTALL); 129 | final ArrayList funcs = new ArrayList<>(); 130 | final Matcher matcher = funcPattern.matcher(body); 131 | while (matcher.find()) { 132 | funcs.add(matcher.group(1)); 133 | } 134 | 135 | 136 | try (FileWriter fileWriter = new FileWriter(source)) { 137 | final String doc = matcherResolver.replaceAll("typedef struct {\n" + 138 | randomList(funcs) + 139 | "} vmResolver;"); 140 | fileWriter.write(doc); 141 | } 142 | } 143 | } 144 | 145 | private static void writeRandomJNIWrapper(File file) throws IOException { 146 | final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( 147 | new FileInputStream(file), StandardCharsets.UTF_8)); 148 | String lines = bufferedReader.lines().collect(Collectors.joining("\n")); 149 | final Pattern p = Pattern.compile("typedef struct \\{([^}]*?)} JNIWrapper;", Pattern.MULTILINE | Pattern.DOTALL); 150 | final Matcher matcherWrapper = p.matcher(lines); 151 | if (matcherWrapper.find()) { 152 | final String body = matcherWrapper.group(1); 153 | //match function pointer 154 | final Pattern funcPattern = Pattern.compile("([^();]* \\**\\(\\*[a-zA-z0-9]*\\)\\([^();]*\\);)", Pattern.MULTILINE | Pattern.DOTALL); 155 | final ArrayList funcs = new ArrayList<>(); 156 | final Matcher matcher = funcPattern.matcher(body); 157 | while (matcher.find()) { 158 | funcs.add(matcher.group(1)); 159 | } 160 | try (FileWriter fileWriter = new FileWriter(file)) { 161 | final String doc = matcherWrapper.replaceAll("typedef struct {\n" + 162 | randomList(funcs) + 163 | "} JNIWrapper;"); 164 | fileWriter.write(doc); 165 | } 166 | } 167 | } 168 | 169 | private static @NotNull String randomList(@NotNull List list) { 170 | final StringBuilder sb = new StringBuilder(); 171 | final int size = list.size(); 172 | for (int i = 0; i < size; i++) { 173 | final Random random = new Random(); 174 | final int idx = random.nextInt(list.size()); 175 | sb.append(list.get(idx)); 176 | sb.append('\n'); 177 | list.remove(idx); 178 | } 179 | return sb.toString(); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/util/FileHelper.kt: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.util 2 | 3 | import java.io.File 4 | import java.io.IOException 5 | import java.io.InputStream 6 | import java.io.OutputStream 7 | import java.nio.charset.Charset 8 | import java.nio.charset.StandardCharsets 9 | 10 | object FileHelper { 11 | @JvmStatic 12 | fun bytesToInputStream(byteArray: ByteArray): InputStream { 13 | return byteArray.inputStream() 14 | } 15 | 16 | @JvmStatic 17 | fun readBytes(file: File): ByteArray { 18 | return file.readBytes() 19 | } 20 | 21 | @JvmStatic 22 | fun readFile(file: File, encoding: Charset): String { 23 | return file.inputStream().readBytes().toString(encoding) 24 | } 25 | 26 | @JvmStatic 27 | fun readFile(path: String, encoding: Charset): String { 28 | return File(path).inputStream().readBytes().toString(encoding) 29 | } 30 | 31 | @JvmStatic 32 | fun writeToFile(file: File, content: String) { 33 | file.writeBytes(content.toByteArray(StandardCharsets.UTF_8)) 34 | } 35 | 36 | @JvmStatic 37 | fun writeToFile(file: File, inputStream: InputStream) { 38 | file.writeBytes(inputStream.readBytes()) 39 | } 40 | 41 | @JvmStatic 42 | fun writeToFile(path: String, content: String) { 43 | File(path).writeBytes(content.toByteArray(StandardCharsets.UTF_8)) 44 | } 45 | 46 | @JvmStatic 47 | fun writeToFile(path: String, inputStream: InputStream) { 48 | File(path).writeBytes(inputStream.readBytes()) 49 | } 50 | 51 | @JvmStatic 52 | @Throws(IOException::class) 53 | fun copyStream(inputStream: InputStream, out: OutputStream) { 54 | val buf = ByteArray(4 * 1024) 55 | var len: Int 56 | while (inputStream.read(buf).also { len = it } != -1) { 57 | out.write(buf, 0, len) 58 | } 59 | } 60 | 61 | @JvmStatic 62 | fun deleteFile(file: File) { 63 | file.deleteRecursively() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/util/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.util; 2 | 3 | import java.io.File; 4 | import java.net.URISyntaxException; 5 | import java.security.CodeSource; 6 | 7 | public class FileUtils { 8 | public static String getHomePath() { 9 | CodeSource codeSource = FileUtils.class.getProtectionDomain().getCodeSource(); 10 | try { 11 | File jarFile = new File(codeSource.getLocation().toURI().getPath()); 12 | return jarFile.getParentFile().getPath(); 13 | } catch (URISyntaxException e) { 14 | e.printStackTrace(); 15 | return getHomePath(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/util/ModifiedUtf8.java: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.util; 2 | 3 | import java.io.UTFDataFormatException; 4 | 5 | //来自java.nio.charset.ModifiedUtf8 6 | public class ModifiedUtf8 { 7 | private ModifiedUtf8() { 8 | } 9 | 10 | /** 11 | * Decodes a byte array containing modified UTF-8 bytes into a string. 12 | * 13 | *

Note that although this method decodes the (supposedly impossible) zero byte to U+0000, 14 | * that's what the RI does too. 15 | */ 16 | public static String decode(byte[] in, char[] out, int offset, int utfSize) throws UTFDataFormatException { 17 | int count = 0, s = 0, a; 18 | while (count < utfSize) { 19 | if ((out[s] = (char) in[offset + count++]) < '\u0080') { 20 | s++; 21 | } else if (((a = out[s]) & 0xe0) == 0xc0) { 22 | if (count >= utfSize) { 23 | throw new UTFDataFormatException("bad second byte at " + count); 24 | } 25 | int b = in[offset + count++]; 26 | if ((b & 0xC0) != 0x80) { 27 | throw new UTFDataFormatException("bad second byte at " + (count - 1)); 28 | } 29 | out[s++] = (char) (((a & 0x1F) << 6) | (b & 0x3F)); 30 | } else if ((a & 0xf0) == 0xe0) { 31 | if (count + 1 >= utfSize) { 32 | throw new UTFDataFormatException("bad third byte at " + (count + 1)); 33 | } 34 | int b = in[offset + count++]; 35 | int c = in[offset + count++]; 36 | if (((b & 0xC0) != 0x80) || ((c & 0xC0) != 0x80)) { 37 | throw new UTFDataFormatException("bad second or third byte at " + (count - 2)); 38 | } 39 | out[s++] = (char) (((a & 0x0F) << 12) | ((b & 0x3F) << 6) | (c & 0x3F)); 40 | } else { 41 | throw new UTFDataFormatException("bad byte at " + (count - 1)); 42 | } 43 | } 44 | return new String(out, 0, s); 45 | } 46 | 47 | /** 48 | * Returns the number of bytes the modified UTF8 representation of 's' would take. Note 49 | * that this is just the space for the bytes representing the characters, not the length 50 | * which precedes those bytes, because different callers represent the length differently, 51 | * as two, four, or even eight bytes. If {@code shortLength} is true, we'll throw an 52 | * exception if the string is too long for its length to be represented by a short. 53 | */ 54 | public static long countBytes(String s, boolean shortLength) throws UTFDataFormatException { 55 | long result = 0; 56 | final int length = s.length(); 57 | for (int i = 0; i < length; ++i) { 58 | char ch = s.charAt(i); 59 | if (ch != 0 && ch <= 127) { // U+0000 uses two bytes. 60 | ++result; 61 | } else if (ch <= 2047) { 62 | result += 2; 63 | } else { 64 | result += 3; 65 | } 66 | if (shortLength && result > 65535) { 67 | throw new UTFDataFormatException("String more than 65535 UTF bytes long"); 68 | } 69 | } 70 | return result; 71 | } 72 | 73 | /** 74 | * Encodes the modified UTF-8 bytes corresponding to string {@code s} into the 75 | * byte array {@code dst}, starting at the given {@code offset}. 76 | */ 77 | public static void encode(byte[] dst, int offset, String s) { 78 | final int length = s.length(); 79 | for (int i = 0; i < length; i++) { 80 | char ch = s.charAt(i); 81 | if (ch != 0 && ch <= 127) { // U+0000 uses two bytes. 82 | dst[offset++] = (byte) ch; 83 | } else if (ch <= 2047) { 84 | dst[offset++] = (byte) (0xc0 | (0x1f & (ch >> 6))); 85 | dst[offset++] = (byte) (0x80 | (0x3f & ch)); 86 | } else { 87 | dst[offset++] = (byte) (0xe0 | (0x0f & (ch >> 12))); 88 | dst[offset++] = (byte) (0x80 | (0x3f & (ch >> 6))); 89 | dst[offset++] = (byte) (0x80 | (0x3f & ch)); 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Returns an array containing the modified UTF-8 form of {@code s}. 96 | * Throws UTFDataFormatException if {@code s} is too long 97 | * for a two-byte length. 98 | */ 99 | public static byte[] encode(String s) throws UTFDataFormatException { 100 | int utfCount = (int) ModifiedUtf8.countBytes(s, true); 101 | byte[] result = new byte[utfCount]; 102 | ModifiedUtf8.encode(result, 0, s); 103 | return result; 104 | } 105 | } -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/util/OsDetector.kt: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.util 2 | 3 | import java.util.* 4 | 5 | object OsDetector { 6 | @JvmStatic 7 | val isWindows: Boolean 8 | get() { 9 | val osName = System.getProperty("os.name") ?: return false 10 | return osName.lowercase(Locale.getDefault()).contains("windows") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/util/Pair.kt: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.util 2 | 3 | class Pair( 4 | @JvmField 5 | val first: T, 6 | @JvmField 7 | val second: V 8 | ) 9 | -------------------------------------------------------------------------------- /library/src/main/java/com/nmmedit/apkprotect/util/ZipHelper.kt: -------------------------------------------------------------------------------- 1 | package com.nmmedit.apkprotect.util 2 | 3 | import com.mcal.apkparser.zip.ZipFile 4 | import java.io.* 5 | import java.util.* 6 | import java.util.regex.Pattern 7 | 8 | object ZipHelper { 9 | /** 10 | * Returns true if there exists a file whose name matches `filename` in `apkFile`. 11 | */ 12 | @JvmStatic 13 | @Throws(IOException::class) 14 | fun hasFile(apkFile: File, filename: String): Boolean { 15 | ZipFile(apkFile).use { apkZip -> return apkZip.getEntry(filename) != null } 16 | } 17 | 18 | /** 19 | * Возвращает содержимое файла с указанным именем из Zip-архива. 20 | * 21 | * @param zipFile Zip-архив, из которого нужно прочитать файл. 22 | * @param filename Имя файла, который нужно прочитать. 23 | * @return Массив байтов, содержащий содержимое файла. 24 | * @throws IOException Если произошла ошибка при чтении файла из Zip-архива. 25 | */ 26 | @JvmStatic 27 | @Throws(IOException::class) 28 | fun getZipFileContent(zipFile: File, filename: String): ByteArray? { 29 | ZipFile(zipFile).use { 30 | it.getInputStream(it.getEntry(filename))?.use { inputStream -> 31 | return inputStream.readBytes() 32 | } 33 | return null 34 | } 35 | } 36 | 37 | /** 38 | * Returns all files in an apk that match a given regular expression. 39 | * 40 | * @param apkFile The file containing the apk zip archive. 41 | * @param regex A regular expression to match the requested filenames. 42 | * @return A mapping of the matched filenames to their byte contents. 43 | * @throws IOException Thrown if a matching file cannot be read from the apk. 44 | */ 45 | @JvmStatic 46 | @Throws(IOException::class) 47 | fun getFiles(apkFile: File, regex: String): Map { 48 | return getFiles(apkFile, Pattern.compile(regex)) 49 | } 50 | 51 | /** 52 | * Returns all files in an apk that match a given regular expression. 53 | * 54 | * @param apkFile The file containing the apk zip archive. 55 | * @param regex A regular expression to match the requested filenames. 56 | * @return A mapping of the matched filenames to their byte contents. 57 | * @throws IOException Thrown if a matching file cannot be read from the apk. 58 | */ 59 | @JvmStatic 60 | @Throws(IOException::class) 61 | fun getFiles(apkFile: File, regex: Pattern): Map { 62 | ZipFile(apkFile).use { apkZip -> 63 | val result = LinkedHashMap() 64 | val entries = apkZip.entries 65 | while (entries.hasMoreElements()) { 66 | val entry = entries.nextElement() 67 | if (regex.matcher(entry.name).matches()) { 68 | apkZip.getInputStream(entry).use { inputStream -> 69 | result.put( 70 | entry.name, 71 | inputStream.readBytes() 72 | ) 73 | } 74 | } 75 | } 76 | return result 77 | } 78 | } 79 | 80 | @JvmStatic 81 | @Throws(IOException::class) 82 | fun extractFiles(apkFile: File, regex: String, outDir: File): List { 83 | return extractFiles(apkFile, Pattern.compile(regex), outDir) 84 | } 85 | 86 | @JvmStatic 87 | @Throws(IOException::class) 88 | fun extractFiles(apkFile: File, regex: Pattern, outDir: File): List { 89 | ZipFile(apkFile).use { apkZip -> 90 | val result = LinkedList() 91 | val entries = apkZip.entries 92 | while (entries.hasMoreElements()) { 93 | val entry = entries.nextElement() 94 | if (!entry.isDirectory && regex.matcher(entry.name).matches()) { 95 | val file = File(outDir, entry.name) 96 | if (!file.parentFile.exists()) { 97 | file.parentFile.mkdirs() 98 | } 99 | apkZip.getInputStream(entry).use { inputStream -> 100 | FileOutputStream(file).use { output -> 101 | copyStream(inputStream, output) 102 | result.add(file) 103 | } 104 | } 105 | } 106 | } 107 | return result 108 | } 109 | } 110 | 111 | @JvmStatic 112 | @Throws(IOException::class) 113 | fun copyStream(inputStream: InputStream, out: OutputStream) { 114 | val buf = ByteArray(8 * 1024) 115 | var len: Int 116 | while (inputStream.read(buf).also { len = it } != -1) { 117 | out.write(buf, 0, len) 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /library/src/main/proto/Configuration.proto: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright (C) 2017 The Android Open Source Project 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | syntax = "proto3"; 18 | package aapt.pb; 19 | option java_package = "com.android.aapt"; 20 | // A description of the requirements a device must have in order for a 21 | // resource to be matched and selected. 22 | message Configuration { 23 | enum LayoutDirection { 24 | LAYOUT_DIRECTION_UNSET = 0; 25 | LAYOUT_DIRECTION_LTR = 1; 26 | LAYOUT_DIRECTION_RTL = 2; 27 | } 28 | enum ScreenLayoutSize { 29 | SCREEN_LAYOUT_SIZE_UNSET = 0; 30 | SCREEN_LAYOUT_SIZE_SMALL = 1; 31 | SCREEN_LAYOUT_SIZE_NORMAL = 2; 32 | SCREEN_LAYOUT_SIZE_LARGE = 3; 33 | SCREEN_LAYOUT_SIZE_XLARGE = 4; 34 | } 35 | enum ScreenLayoutLong { 36 | SCREEN_LAYOUT_LONG_UNSET = 0; 37 | SCREEN_LAYOUT_LONG_LONG = 1; 38 | SCREEN_LAYOUT_LONG_NOTLONG = 2; 39 | } 40 | enum ScreenRound { 41 | SCREEN_ROUND_UNSET = 0; 42 | SCREEN_ROUND_ROUND = 1; 43 | SCREEN_ROUND_NOTROUND = 2; 44 | } 45 | enum WideColorGamut { 46 | WIDE_COLOR_GAMUT_UNSET = 0; 47 | WIDE_COLOR_GAMUT_WIDECG = 1; 48 | WIDE_COLOR_GAMUT_NOWIDECG = 2; 49 | } 50 | enum Hdr { 51 | HDR_UNSET = 0; 52 | HDR_HIGHDR = 1; 53 | HDR_LOWDR = 2; 54 | } 55 | enum Orientation { 56 | ORIENTATION_UNSET = 0; 57 | ORIENTATION_PORT = 1; 58 | ORIENTATION_LAND = 2; 59 | ORIENTATION_SQUARE = 3; 60 | } 61 | enum UiModeType { 62 | UI_MODE_TYPE_UNSET = 0; 63 | UI_MODE_TYPE_NORMAL = 1; 64 | UI_MODE_TYPE_DESK = 2; 65 | UI_MODE_TYPE_CAR = 3; 66 | UI_MODE_TYPE_TELEVISION = 4; 67 | UI_MODE_TYPE_APPLIANCE = 5; 68 | UI_MODE_TYPE_WATCH = 6; 69 | UI_MODE_TYPE_VRHEADSET = 7; 70 | } 71 | enum UiModeNight { 72 | UI_MODE_NIGHT_UNSET = 0; 73 | UI_MODE_NIGHT_NIGHT = 1; 74 | UI_MODE_NIGHT_NOTNIGHT = 2; 75 | } 76 | enum Touchscreen { 77 | TOUCHSCREEN_UNSET = 0; 78 | TOUCHSCREEN_NOTOUCH = 1; 79 | TOUCHSCREEN_STYLUS = 2; 80 | TOUCHSCREEN_FINGER = 3; 81 | } 82 | enum KeysHidden { 83 | KEYS_HIDDEN_UNSET = 0; 84 | KEYS_HIDDEN_KEYSEXPOSED = 1; 85 | KEYS_HIDDEN_KEYSHIDDEN = 2; 86 | KEYS_HIDDEN_KEYSSOFT = 3; 87 | } 88 | enum Keyboard { 89 | KEYBOARD_UNSET = 0; 90 | KEYBOARD_NOKEYS = 1; 91 | KEYBOARD_QWERTY = 2; 92 | KEYBOARD_TWELVEKEY = 3; 93 | } 94 | enum NavHidden { 95 | NAV_HIDDEN_UNSET = 0; 96 | NAV_HIDDEN_NAVEXPOSED = 1; 97 | NAV_HIDDEN_NAVHIDDEN = 2; 98 | } 99 | enum Navigation { 100 | NAVIGATION_UNSET = 0; 101 | NAVIGATION_NONAV = 1; 102 | NAVIGATION_DPAD = 2; 103 | NAVIGATION_TRACKBALL = 3; 104 | NAVIGATION_WHEEL = 4; 105 | } 106 | // 107 | // Axis/dimensions that are understood by the runtime. 108 | // 109 | // Mobile country code. 110 | uint32 mcc = 1; 111 | // Mobile network code. 112 | uint32 mnc = 2; 113 | // BCP-47 locale tag. 114 | string locale = 3; 115 | // Left-to-right, right-to-left... 116 | LayoutDirection layout_direction = 4; 117 | // Screen width in pixels. Prefer screen_width_dp. 118 | uint32 screen_width = 5; 119 | // Screen height in pixels. Prefer screen_height_dp. 120 | uint32 screen_height = 6; 121 | // Screen width in density independent pixels (dp). 122 | uint32 screen_width_dp = 7; 123 | // Screen height in density independent pixels (dp). 124 | uint32 screen_height_dp = 8; 125 | // The smallest screen dimension, regardless of orientation, in dp. 126 | uint32 smallest_screen_width_dp = 9; 127 | // Whether the device screen is classified as small, normal, large, xlarge. 128 | ScreenLayoutSize screen_layout_size = 10; 129 | // Whether the device screen is long. 130 | ScreenLayoutLong screen_layout_long = 11; 131 | // Whether the screen is round (Android Wear). 132 | ScreenRound screen_round = 12; 133 | // Whether the screen supports wide color gamut. 134 | WideColorGamut wide_color_gamut = 13; 135 | // Whether the screen has high dynamic range. 136 | Hdr hdr = 14; 137 | // Which orientation the device is in (portrait, landscape). 138 | Orientation orientation = 15; 139 | // Which type of UI mode the device is in (television, car, etc.). 140 | UiModeType ui_mode_type = 16; 141 | // Whether the device is in night mode. 142 | UiModeNight ui_mode_night = 17; 143 | // The device's screen density in dots-per-inch (dpi). 144 | uint32 density = 18; 145 | // Whether a touchscreen exists, supports a stylus, or finger. 146 | Touchscreen touchscreen = 19; 147 | // Whether the keyboard hardware keys are currently hidden, exposed, or 148 | // if the keyboard is a software keyboard. 149 | KeysHidden keys_hidden = 20; 150 | // The type of keyboard present (none, QWERTY, 12-key). 151 | Keyboard keyboard = 21; 152 | // Whether the navigation is exposed or hidden. 153 | NavHidden nav_hidden = 22; 154 | // The type of navigation present on the device 155 | // (trackball, wheel, dpad, etc.). 156 | Navigation navigation = 23; 157 | // The minimum SDK version of the device. 158 | uint32 sdk_version = 24; 159 | // 160 | // Build-time only dimensions. 161 | // 162 | string product = 25; 163 | } -------------------------------------------------------------------------------- /library/src/main/proto/apex_manifest.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2019 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // This file should be in sync with 18 | // https://android.googlesource.com/platform/system/apex/+/refs/heads/master/proto/apex_manifest.proto 19 | // But, since bundletool itself doesn't need to read other than name, 20 | // let's keep this as minimal. 21 | 22 | syntax = "proto3"; 23 | 24 | package apex.proto; 25 | 26 | option java_package = "com.android.apex"; 27 | option java_outer_classname = "ApexManifestProto"; 28 | 29 | message ApexManifest { 30 | 31 | // Package Name 32 | string name = 1; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /library/src/main/proto/app_dependencies.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package android.bundle; 4 | 5 | import "google/protobuf/wrappers.proto"; 6 | 7 | option java_package = "com.android.bundle"; 8 | 9 | // Lists the dependencies of an application. 10 | message AppDependencies { 11 | // List of all the dependencies, direct and indirect. 12 | repeated Library library = 1; 13 | 14 | // Dependencies of the libraries from the list above. 15 | repeated LibraryDependencies library_dependencies = 2; 16 | 17 | // List of direct dependencies per bundle module. 18 | repeated ModuleDependencies module_dependencies = 3; 19 | 20 | // List of repositories where dependencies were found. 21 | repeated Repository repositories = 4; 22 | } 23 | 24 | // List of dependencies of a given library. 25 | message LibraryDependencies { 26 | // Indices correspond to the pool of libraries defined in AppDependencies. 27 | optional int32 library_index = 1; 28 | repeated int32 library_dep_index = 2; 29 | } 30 | 31 | // Lists the dependencies of a given module. 32 | message ModuleDependencies { 33 | optional string module_name = 1; 34 | // Direct module dependencies. 35 | // Index is from the pool of libraries defined in AppDependencies. 36 | repeated int32 dependency_index = 2; 37 | } 38 | 39 | // Next ID: 5 40 | message Library { 41 | // Type of library dependency. 42 | oneof library_oneof { 43 | // A library downloaded from a Maven repository. 44 | MavenLibrary maven_library = 1; 45 | // A library downloaded from a Unity repository. 46 | UnityLibrary unity_library = 4; 47 | } 48 | 49 | // This message contains various digests of the library contents. 50 | message Digests { 51 | // SHA256 hash value of the file contents. 52 | optional bytes sha256 = 1; 53 | } 54 | 55 | optional Digests digests = 2; 56 | 57 | // Repository from which the artifact was retrieved (if known). 58 | // Index is from pool of repositories defined in AppDependencies. 59 | optional google.protobuf.Int32Value repo_index = 3; 60 | } 61 | 62 | message MavenLibrary { 63 | optional string group_id = 1; 64 | optional string artifact_id = 2; 65 | optional string packaging = 3; 66 | optional string classifier = 4; 67 | optional string version = 5; 68 | } 69 | 70 | message UnityLibrary { 71 | // Corresponds to the "name" field in the package.json, uniquely identifying 72 | // the library. 73 | optional string package_name = 1; 74 | 75 | // Corresponds to the "version" field in the package.json file of the version 76 | // of the library actually compiled in the app. 77 | optional string version = 2; 78 | } 79 | 80 | // A repository for resolving artifacts and metadata. 81 | message Repository { 82 | // The type of the repository, and any type-specific configuration info. 83 | oneof repo_oneof { 84 | MavenRepo maven_repo = 1; 85 | IvyRepo ivy_repo = 2; 86 | UnityRepo unity_repo = 3; 87 | } 88 | } 89 | 90 | message MavenRepo { 91 | // The root url for the repository. 92 | optional string url = 1; 93 | } 94 | 95 | message IvyRepo { 96 | // The root url for the repository. 97 | optional string url = 1; 98 | } 99 | 100 | message UnityRepo { 101 | // The root url for the repository. 102 | optional string url = 1; 103 | } 104 | -------------------------------------------------------------------------------- /library/src/main/proto/app_integrity_config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | option java_package = "com.android.bundle"; 6 | 7 | // Specifies integrity protection options that should be applied to an app 8 | // bundle. 9 | // Next tag: 8. 10 | message AppIntegrityConfig { 11 | bool enabled = 1; 12 | LicenseCheck license_check = 2; 13 | InstallerCheck installer_check = 3; 14 | DebuggerCheck debugger_check_deprecated = 4 [deprecated = true]; 15 | EmulatorCheck emulator_check = 5; 16 | // Optional. If present, java/kotlin code will be obfuscated according to the 17 | // config. 18 | DexProtectionConfig dex_protection_config = 6; 19 | string version_label = 7; 20 | } 21 | 22 | // Next tag: 4. 23 | message LicenseCheck { 24 | bool enabled = 1; 25 | bool online_only = 2; 26 | Policy policy = 3; 27 | } 28 | 29 | // Next tag: 4. 30 | message InstallerCheck { 31 | bool enabled = 1; 32 | Policy policy = 2; 33 | repeated string additional_install_source = 3; 34 | } 35 | 36 | // Next tag: 2 37 | message DebuggerCheck { 38 | option deprecated = true; 39 | 40 | bool enabled = 1; 41 | } 42 | 43 | // Next tag: 2 44 | message EmulatorCheck { 45 | bool enabled = 1; 46 | } 47 | 48 | // Next tag: 2 49 | message Policy { 50 | enum Action { 51 | UNSPECIFIED = 0; 52 | WARN = 1; 53 | DISABLE = 2; 54 | WARN_THEN_DISABLE = 3; 55 | } 56 | Action action = 1; 57 | } 58 | 59 | // Configuration of java-related obfuscation. 60 | message DexProtectionConfig { 61 | bool enabled = 1; 62 | // Either fully qualified method reference e.g. 63 | // `java.lang.MyClass#myMethod(int,float,java.lang.String)`, or a partially 64 | // qualified reference e.g. `java.lang.MyClass#myMethod`. 65 | repeated string method_to_obfuscate = 2; 66 | 67 | // Describes how to look for methods to obfuscate. 68 | enum TargetingMode { 69 | TARGETING_MODE_UNSPECIFIED = 0; 70 | // We'll try to automatically find methods for obfuscation, in addition to 71 | // the methods specified in this config. 72 | TARGETING_MODE_AUTOMATIC = 1; 73 | // Only methods spefied in this config will be considered for obfuscation. 74 | TARGETING_MODE_PROPOSED_ONLY = 2; 75 | } 76 | TargetingMode targeting_mode = 3; 77 | } 78 | -------------------------------------------------------------------------------- /library/src/main/proto/build_stamp.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | import "google/protobuf/timestamp.proto"; 6 | 7 | option java_package = "com.android.bundle"; 8 | 9 | // Build stamp metadata. 10 | message BuildStampFile { 11 | // The build stamp for this bundle. 12 | BuildStamp build_stamp = 1; 13 | } 14 | 15 | // A build stamp. 16 | // Next tag: 8. 17 | message BuildStamp { 18 | // Source revision for the build, e.g. HEAD commit hash. 19 | string source_revision = 1; 20 | // ID of the build job which created this bundle. 21 | string job_id = 2; 22 | // URL to the build job which created this bundle. Does not need to be public, 23 | // and probably will not be. 24 | string job_url = 3; 25 | // ID for the specific build, e.g. a UUID. 26 | string build_id = 4; 27 | // Build label: an arbitrary string set by the build system. May be used to 28 | // embed a release label. 29 | string label = 5; 30 | // Time at which the build was started. 31 | google.protobuf.Timestamp build_start_timestamp = 6; 32 | 33 | // Status of the working tree this bundle was built from. 34 | enum WorktreeStatus { 35 | WORKTREE_STATUS_UNSPECIFIED = 0; 36 | // Clean. No uncommitted modifications or files. 37 | WORKTREE_STATUS_CLEAN = 1; 38 | // Dirty. One or more uncommitted modifications or files. 39 | WORKTREE_STATUS_DIRTY = 2; 40 | } 41 | // Status of the working tree this bundle was built from. 42 | WorktreeStatus worktree_status = 7; 43 | } 44 | -------------------------------------------------------------------------------- /library/src/main/proto/code_transparency.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | option java_package = "com.android.bundle"; 6 | 7 | // Code transparency related metadata. 8 | message CodeTransparency { 9 | // List of code-related files in the bundle. 10 | repeated CodeRelatedFile code_related_file = 1; 11 | 12 | // Version of code transparency file format. 13 | // Required. 14 | int32 version = 2; 15 | } 16 | 17 | message CodeRelatedFile { 18 | enum Type { 19 | TYPE_UNSPECIFIED = 0; 20 | NATIVE_LIBRARY = 1; 21 | DEX = 2; 22 | } 23 | // Code related file may come from either Android App Bundle (AAB) or 24 | // bundletool repo. Bundletool repo is used as a source for a DEX file for an 25 | // archived APK which is not the part AAB and is injected when APKs are 26 | // generated from the AAB. 27 | // Required. 28 | oneof source { 29 | // Path to file in the bundle. 30 | string path = 1; 31 | // Path to a file that will be injected at build time. 32 | // Contained in the bundletool source code. 33 | string bundletool_repo_path = 5; 34 | }; 35 | // Type of code-related file. 36 | // Required. 37 | Type type = 2; 38 | // Path to file in the APK. Only set for NATIVE_LIBRARY types. 39 | string apk_path = 3; 40 | // SHA256 digest of the code-related file. 41 | string sha256 = 4; 42 | } 43 | -------------------------------------------------------------------------------- /library/src/main/proto/device_targeting_config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | option java_package = "com.android.bundle"; 6 | option java_multiple_files = true; 7 | 8 | // Configuration describing device targeting criteria for the content of an app. 9 | message DeviceTierConfig { 10 | // Definition of device groups for the app. 11 | repeated DeviceGroup device_groups = 1; 12 | 13 | // Definition of the set of device tiers for the app. 14 | DeviceTierSet device_tier_set = 2; 15 | 16 | // Definition of user country sets for the app. 17 | repeated UserCountrySet user_country_sets = 5; 18 | } 19 | 20 | // A group of devices. 21 | // 22 | // A group is defined by a set of device selectors. A device belongs to the 23 | // group if it matches any selector (logical OR). 24 | message DeviceGroup { 25 | // The name of the group. 26 | string name = 1; 27 | 28 | // Device selectors for this group. A device matching any of the selectors 29 | // is included in this group. 30 | repeated DeviceSelector device_selectors = 2; 31 | } 32 | 33 | // A set of device tiers. 34 | // 35 | // A tier set determines what variation of app content gets served to a specific 36 | // device, for device-targeted content. 37 | // 38 | // You should assign a priority level to each tier, which determines the 39 | // ordering by which they are evaluated by Play. See the documentation of 40 | // DeviceTier.level for more details. 41 | message DeviceTierSet { 42 | // Device tiers belonging to the set. 43 | repeated DeviceTier device_tiers = 1; 44 | } 45 | 46 | // A set of user countries. 47 | // 48 | // A country set determines what variation of app content gets served to a 49 | // specific location. 50 | message UserCountrySet { 51 | // Country set name. 52 | string name = 1; 53 | 54 | // List of country codes representing countries. 55 | // A Country code is represented in ISO 3166 alpha-2 format. 56 | // For Example:- "IT" for Italy, "GE" for Georgia. 57 | repeated string country_codes = 2; 58 | } 59 | 60 | // A single device tier. 61 | // 62 | // Devices matching any of the device groups in device_group_names are 63 | // considered to match the tier. 64 | message DeviceTier { 65 | // Groups of devices included in this tier. 66 | // These groups must be defined explicitly under device_groups in this 67 | // configuration. 68 | repeated string device_group_names = 1; 69 | 70 | // The priority level of the tier. 71 | // 72 | // Tiers are evaluated in descending order of level: the highest level tier 73 | // has the highest priority. The highest tier matching a given device is 74 | // selected for that device. 75 | // 76 | // You should use a contiguous range of levels for your tiers in a tier set; 77 | // tier levels in a tier set must be unique. 78 | // For instance, if your tier set has 4 tiers (including the global fallback), 79 | // you should define tiers 1, 2 and 3 in this configuration. 80 | // 81 | // Note: tier 0 is implicitly defined as a global fallback and selected for 82 | // devices that don't match any of the tiers explicitly defined here. You 83 | // mustn't define level 0 explicitly in this configuration. 84 | int32 level = 2; 85 | } 86 | 87 | // Selector for a device group. 88 | // A selector consists of a set of conditions on the device that should all 89 | // match (logical AND) to determine a device group eligibility. 90 | // 91 | // For instance, if a selector specifies RAM conditions, device model inclusion 92 | // and device model exclusion, a device is considered to match if: 93 | // device matches RAM conditions 94 | // AND 95 | // device matches one of the included device models 96 | // AND 97 | // device doesn't match excluded device models 98 | message DeviceSelector { 99 | // Conditions on the device's RAM. 100 | DeviceRam device_ram = 1; 101 | 102 | // Device models included by this selector. 103 | repeated DeviceId included_device_ids = 2; 104 | 105 | // Device models excluded by this selector, even if they match all other 106 | // conditions. 107 | repeated DeviceId excluded_device_ids = 3; 108 | 109 | // A device needs to have all these system features to be 110 | // included by the selector. 111 | repeated SystemFeature required_system_features = 4; 112 | 113 | // A device that has any of these system features is excluded by 114 | // this selector, even if it matches all other conditions. 115 | repeated SystemFeature forbidden_system_features = 5; 116 | } 117 | 118 | // Conditions about a device's RAM capabilities. 119 | message DeviceRam { 120 | // Minimum RAM in bytes (bound included). 121 | int64 min_bytes = 1; 122 | 123 | // Maximum RAM in bytes (bound excluded). 124 | int64 max_bytes = 2; 125 | } 126 | 127 | // Identifier of a device. 128 | message DeviceId { 129 | // Value of Build.BRAND. 130 | string build_brand = 1; 131 | 132 | // Value of Build.DEVICE. 133 | string build_device = 2; 134 | } 135 | 136 | // Representation of a system feature. 137 | message SystemFeature { 138 | // The name of the feature. 139 | string name = 1; 140 | } 141 | 142 | // Properties of a particular device. 143 | message DeviceProperties { 144 | // Device RAM in bytes. 145 | int64 ram = 1; 146 | 147 | // Device ID of device. 148 | DeviceId device_id = 2; 149 | 150 | // System features in device. 151 | repeated SystemFeature system_features = 3; 152 | } 153 | -------------------------------------------------------------------------------- /library/src/main/proto/devices.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | import "google/protobuf/wrappers.proto"; 6 | 7 | option java_package = "com.android.bundle"; 8 | 9 | message DeviceSpec { 10 | // Supported ABI architectures in the order of preference. 11 | // The values should be the string as reported by the platform, e.g. 12 | // "armeabi-v7a" or "x86_64". 13 | repeated string supported_abis = 1; 14 | 15 | // All installed locales represented as BCP-47 strings. 16 | repeated string supported_locales = 2; 17 | 18 | // List of device features returned by the package manager utility. 19 | repeated string device_features = 3; 20 | 21 | // List of OpenGL extension strings supported by the device. 22 | repeated string gl_extensions = 4; 23 | 24 | // Screen dpi. 25 | uint32 screen_density = 5; 26 | 27 | // getprop ro.build.version.sdk 28 | uint32 sdk_version = 6; 29 | 30 | // getprop ro.build.version.codename 31 | string codename = 7; 32 | 33 | // Device tier. 34 | google.protobuf.Int32Value device_tier = 8; 35 | 36 | // Device groups the device belongs to. 37 | repeated string device_groups = 9; 38 | 39 | // Information about the runtime-enabled SDK capabilities of the device. 40 | SdkRuntime sdk_runtime = 10; 41 | 42 | // Country set. 43 | google.protobuf.StringValue country_set = 11; 44 | } 45 | 46 | message SdkRuntime { 47 | // Whether runtime-enabled SDKs can run on the device 48 | bool supported = 1; 49 | 50 | reserved 2; 51 | } 52 | -------------------------------------------------------------------------------- /library/src/main/proto/errors.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | option java_package = "com.android.bundle"; 6 | 7 | // Next ID: 30 8 | message BundleToolError { 9 | // The exception message that the bundle tool outputs. 10 | // NOTE: this may not be safe to show to users since it might 11 | // include server paths and configuration. 12 | string exception_message = 1; 13 | 14 | // The exception message that can be exposed to third party developers. 15 | string user_message = 28; 16 | 17 | // Type of error. 18 | ErrorType error_type = 29; 19 | 20 | reserved 2 to 27; 21 | 22 | enum ErrorType { 23 | UNDEFINED = 0; 24 | // Indicates invalid bundle. 25 | INVALID_BUNDLE_ERROR = 1; 26 | // Indicates bundle with invalid version code in AndroidManifest.xml. 27 | INVALID_VERSION_CODE_ERROR = 2; 28 | // Indicates that command is incompatible with requested device. 29 | INCOMPATIBLE_DEVICE_ERROR = 3; 30 | // Indicates invalid device spec provided to command. 31 | INVALID_DEVICE_SPEC_ERROR = 4; 32 | // Indicates that command is invalid, like options mismatch. 33 | INVALID_COMMAND_ERROR = 5; 34 | // Indicates that something happened during command execution. 35 | COMMAND_EXECUTION_ERROR = 6; 36 | // Indicates invalid signing configuration. 37 | INVALID_SIGNING_CONFIGURATION_ERROR = 7; 38 | } 39 | } 40 | 41 | message ManifestMaxSdkInvalidError { 42 | string max_sdk = 1; 43 | } 44 | 45 | message ManifestMaxSdkLessThanMinInstantSdkError { 46 | int32 max_sdk = 1; 47 | } 48 | 49 | message ManifestMinSdkInvalidError { 50 | string min_sdk = 1; 51 | } 52 | 53 | message ManifestMinSdkGreaterThanMaxSdkError { 54 | int32 min_sdk = 1; 55 | int32 max_sdk = 2; 56 | } 57 | 58 | message ManifestMissingVersionCodeError {} 59 | 60 | message ManifestInvalidVersionCodeError {} 61 | 62 | message ManifestBaseModuleExcludedFromFusingError {} 63 | 64 | message ManifestModuleFusingConfigurationMissingError { 65 | string module_name = 1; 66 | } 67 | 68 | message ManifestFusingMissingIncludeAttributeError { 69 | string module_name = 1; 70 | } 71 | 72 | message ManifestDuplicateAttributeError { 73 | string attribute_name = 1; 74 | string module_name = 2; 75 | } 76 | 77 | message ManifestModulesDifferentVersionCodes { 78 | repeated int32 version_codes = 1; 79 | } 80 | 81 | message FileTypeInvalidFileExtensionError { 82 | string bundle_directory = 1; 83 | string required_extension = 2; 84 | string invalid_file = 3; 85 | } 86 | 87 | message FileTypeInvalidFileNameInDirectoryError { 88 | string bundle_directory = 1; 89 | repeated string allowed_file_name = 2; 90 | string invalid_file = 3; 91 | } 92 | 93 | message FileTypeInvalidNativeLibraryPathError { 94 | string bundle_directory = 1; 95 | string invalid_file = 2; 96 | } 97 | 98 | message FileTypeInvalidApexImagePathError { 99 | string bundle_directory = 1; 100 | string invalid_file = 2; 101 | } 102 | 103 | message FileTypeInvalidNativeArchitectureError { 104 | string invalid_architecture_directory = 1; 105 | } 106 | 107 | message FileTypeFilesInResourceDirectoryRootError { 108 | string resource_directory = 1; 109 | string invalid_file = 2; 110 | } 111 | 112 | message FileTypeUnknownFileOrDirectoryFoundInModuleError { 113 | string invalid_file = 1; 114 | } 115 | 116 | message FileTypeFileUsesReservedNameError { 117 | string invalid_file = 1; 118 | } 119 | 120 | message FileTypeDirectoryInBundleError { 121 | string invalid_directory = 1; 122 | } 123 | 124 | message MandatoryBundleFileMissingError { 125 | string missing_file = 1; 126 | } 127 | 128 | message MandatoryModuleFileMissingError { 129 | string module_name = 1; 130 | string missing_file = 2; 131 | } 132 | 133 | message ResourceTableReferencesFilesOutsideResError { 134 | string module_name = 1; 135 | string file_path = 2; 136 | } 137 | 138 | message ResourceTableReferencesMissingFilesError { 139 | string module_name = 1; 140 | repeated string file_path = 2; 141 | } 142 | 143 | message ResourceTableUnreferencedFilesError { 144 | string module_name = 1; 145 | repeated string file_path = 2; 146 | } 147 | 148 | message ResourceTableMissingError { 149 | string module_name = 1; 150 | } 151 | -------------------------------------------------------------------------------- /library/src/main/proto/files.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | import "targeting.proto"; 6 | 7 | option java_package = "com.android.bundle"; 8 | 9 | // Describes the assets in the App Bundle. 10 | message Assets { 11 | // List of all the directories (and subdirectories recursively) under 12 | // "assets/" that contain at least one file. They may be explicitly targeted 13 | // or not. 14 | repeated TargetedAssetsDirectory directory = 1; 15 | } 16 | 17 | // Describes the native libraries in the App Bundle. 18 | message NativeLibraries { 19 | // List of all the directories in under "lib/" that contain at least one file. 20 | repeated TargetedNativeDirectory directory = 1; 21 | } 22 | 23 | // Describes the APEX images in the App Bundle. 24 | message ApexImages { 25 | reserved 2; 26 | 27 | // List of all the image files under "apex/". 28 | repeated TargetedApexImage image = 1; 29 | } 30 | 31 | // An assets directory in the module that contains targeting information. 32 | // This information will be used when generating the APKs to determine how to 33 | // best split them. 34 | message TargetedAssetsDirectory { 35 | // Path of the directory relative to the root of the module. 36 | // Required. 37 | string path = 1; 38 | 39 | // Targeting of the directory. 40 | // Required. 41 | AssetsDirectoryTargeting targeting = 2; 42 | } 43 | 44 | // A native directory in the module that contains targeting information. 45 | // This information will be used when generating the APKs to determine how to 46 | // best split them. 47 | message TargetedNativeDirectory { 48 | // Path of the directory relative to the root of the module. 49 | // Required. 50 | string path = 1; 51 | 52 | // Targeting of the directory. 53 | // Required. 54 | NativeDirectoryTargeting targeting = 2; 55 | } 56 | 57 | // An APEX image file in the module that contains targeting information. 58 | // This information will be used when generating the APK to determine which 59 | // image it contains. 60 | message TargetedApexImage { 61 | string path = 1; 62 | 63 | // Path to binary protocol buffer file containing apex.proto.ApexBuildInfo 64 | // with metadata about how the individual APEX filesystem image was built. 65 | // May be empty if APEX build info was not provided for this image. 66 | string build_info_path = 3; 67 | 68 | ApexImageTargeting targeting = 2; 69 | } 70 | -------------------------------------------------------------------------------- /library/src/main/proto/rotation_config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | option java_package = "com.android.bundle"; 6 | 7 | // Specifies the config that gets applied to the rotation aspect of the signing 8 | // process of the App Bundle. 9 | // Next tag: 3 10 | message RotationConfig { 11 | // The SHA256 fingerprint of the expected certificate to sign the APKs 12 | // generated from the Bundle. 13 | // Example: 14 | // FE:C0:E6:5B:F3:76:5D:A1:C2:56:13:C7:A3:60:35:A9:26:BC:3B:3A:39:9B:C8:55:40:F1:6D:55:17:3F:F5:9B 15 | string signing_certificate_sha256_fingerprint = 1; 16 | 17 | // The minimum API level for which an APK's rotated signing key should be used 18 | // to produce the APK's signature. The original signing key for the APK will 19 | // be used for all previous platform versions. 20 | int32 rotation_min_sdk_version = 2; 21 | } 22 | -------------------------------------------------------------------------------- /library/src/main/proto/runtime_enabled_sdk_config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | option java_package = "com.android.bundle"; 6 | option java_outer_classname = "RuntimeEnabledSdkConfigProto"; 7 | 8 | // Information about runtime-enabled SDK dependencies of a specific module 9 | // inside the App Bundle. 10 | message RuntimeEnabledSdkConfig { 11 | // Runtime-enabled SDKs in this config. 12 | repeated RuntimeEnabledSdk runtime_enabled_sdk = 1; 13 | } 14 | 15 | message RuntimeEnabledSdk { 16 | // Package name of the runtime-enabled SDK. 17 | string package_name = 1; 18 | // Major version of the runtime-enabled SDK. 19 | int32 version_major = 2; 20 | // Minor version of the runtime-enabled SDK. 21 | int32 version_minor = 3; 22 | // Patch version of the runtime-enabled SDK. 23 | // The dependency on a specific patch version is a build-time soft dependency, 24 | // that ensures reproducibility of local builds; it does not imply that all 25 | // app stores will honour it when delivering apps to end-users. For instance, 26 | // some stores may just honour the dependency on a specific major and minor, 27 | // while serve the latest available patch for the given major.minor version of 28 | // the SDK. 29 | int32 build_time_version_patch = 6; 30 | // SHA-256 hash of the runtime-enabled SDK's signing certificate, represented 31 | // as a string of bytes in hexadecimal form, with ':' separating the bytes. 32 | string certificate_digest = 4; 33 | // Package ID that the resource IDs of this runtime-enabled SDK must be 34 | // remapped to. 35 | int32 resources_package_id = 5; 36 | } 37 | 38 | // Options for overriding parts of runtime-enabled SDK dependency configuration 39 | // for local deployment and testing. 40 | message LocalDeploymentRuntimeEnabledSdkConfig { 41 | // Options for overriding SDK certificate digests in RuntimeEnabledSdkConfig 42 | // of the app bundle. 43 | CertificateOverrides certificate_overrides = 1; 44 | } 45 | 46 | // Contains information on certificate digests that should be overridden in 47 | // runtime-enabled SDK dependency config for local deployment. 48 | message CertificateOverrides { 49 | // Certificate digest override for specific SDKs. 50 | repeated CertificateOverride per_sdk_certificate_override = 1; 51 | // Default certificate override. If set, certificate digests of SDKs that are 52 | // not present in per_sdk_certificate_override field will be overriden to this 53 | // value. 54 | optional string default_certificate_override = 2; 55 | } 56 | 57 | // Contains information on certificate digest overrides for a specific SDK. 58 | message CertificateOverride { 59 | // Package name of the SDK that we want to override the certificate digest 60 | // for. 61 | string sdk_package_name = 1; 62 | // Certificate digest that we should override to. 63 | string certificate_digest = 2; 64 | } 65 | 66 | // Properties that backwards-compatible SDK split inherits from the app. 67 | message SdkSplitPropertiesInheritedFromApp { 68 | // Package name of the app. 69 | string package_name = 1; 70 | // Version code of the app. 71 | int32 version_code = 2; 72 | // minSdkVersion of the app. 73 | int32 min_sdk_version = 3; 74 | // Package ID that the SDK resources should be remapped to so that they do not 75 | // conflict with the app's resources. 76 | int32 resources_package_id = 4; 77 | } 78 | -------------------------------------------------------------------------------- /library/src/main/proto/sdk_bundle_config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | option java_package = "com.android.bundle"; 6 | option java_outer_classname = "SdkBundleConfigProto"; 7 | 8 | message SdkBundleConfig { 9 | // Runtime-enabled SDKs this SDK depends on. 10 | repeated SdkBundle sdk_dependencies = 1; 11 | } 12 | 13 | message SdkBundle { 14 | // Package name of the SDK bundle. 15 | string package_name = 1; 16 | 17 | // Major version of the SDK bundle. 18 | int32 version_major = 2; 19 | 20 | // Minor version of the SDK bundle. 21 | int32 version_minor = 3; 22 | 23 | // Patch version of the SDK bundle. 24 | // The dependency on a specific patch version is a build-time soft dependency, 25 | // that ensures reproducibility of local builds; it does not imply that all 26 | // app stores will honour it when delivering apps to end-users. For instance, 27 | // some stores may just honour the dependency on a specific major and minor, 28 | // while serve the latest available patch for the given major.minor version of 29 | // the SDK. 30 | int32 build_time_version_patch = 4; 31 | 32 | // SHA-256 hash of the SDK's signing certificate, represented as a string of 33 | // bytes in hexadecimal form, with ':' separating the bytes. 34 | string certificate_digest = 5; 35 | 36 | // Whether the dependency is optional or required. Only required dependencies 37 | // will be included in the final POM file. 38 | // Unspecified dependency types will be treated as required. 39 | SdkDependencyType dependency_type = 6; 40 | } 41 | 42 | enum SdkDependencyType { 43 | SDK_DEPENDENCY_TYPE_UNSPECIFIED = 0; 44 | // The dependency should be installed automatically. 45 | SDK_DEPENDENCY_TYPE_REQUIRED = 1; 46 | // The dependency is only needed at compile time. 47 | SDK_DEPENDENCY_TYPE_OPTIONAL = 2; 48 | } 49 | -------------------------------------------------------------------------------- /library/src/main/proto/sdk_metadata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | import "sdk_modules_config.proto"; 6 | 7 | option java_package = "com.android.bundle"; 8 | 9 | // Information that uniquely identifies this runtime-enabled SDK version. 10 | // This will be stored inside the ASAR for an SDK. 11 | message SdkMetadata { 12 | // Package name of the runtime-enabled SDK. 13 | string package_name = 1; 14 | 15 | // Version information for the runtime-enabled SDK. 16 | RuntimeEnabledSdkVersion sdk_version = 2; 17 | 18 | // SHA-256 hash of the runtime-enabled SDK's signing certificate, represented 19 | // as a string of bytes in hexadecimal form, with ':' separating the bytes. 20 | string certificate_digest = 3; 21 | } 22 | -------------------------------------------------------------------------------- /library/src/main/proto/sdk_modules_config.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | import "config.proto"; 6 | 7 | option java_package = "com.android.bundle"; 8 | 9 | // Contains the package name and versioning information of the Android SDK 10 | // Bundle. These are required to generate the installable APKs. 11 | message SdkModulesConfig { 12 | // Version of BundleTool used to generate the Android SDK Bundle. 13 | Bundletool bundletool = 1; 14 | 15 | // Package name of the runtime-enabled SDK. 16 | string sdk_package_name = 2; 17 | 18 | // Version of the runtime-enabled SDK. 19 | RuntimeEnabledSdkVersion sdk_version = 3; 20 | 21 | // The fully qualified name for the platform SDK provider entrypoint class. 22 | string sdk_provider_class_name = 4; 23 | 24 | // The fully qualified name for the compatibility SDK provider entrypoint 25 | // class. 26 | // Expected to be set for SDKs that support devices without the privacy 27 | // sandbox. 28 | string compat_sdk_provider_class_name = 5; 29 | } 30 | 31 | message RuntimeEnabledSdkVersion { 32 | // The major version of the RE SDK. 33 | int32 major = 1; 34 | 35 | // The minor version of the RE SDK. 36 | int32 minor = 2; 37 | 38 | // The patch version of the RE SDK. 39 | int32 patch = 3; 40 | } 41 | -------------------------------------------------------------------------------- /library/src/main/proto/sizes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | option java_package = "com.android.bundle"; 6 | 7 | // Sizes of the breakdown item: bytes in the APK, and bytes over the wire. 8 | message Sizes { 9 | int64 disk_size = 1; 10 | int64 download_size = 2; 11 | } 12 | 13 | // Breakdown of APK, or an aggregate. 14 | message Breakdown { 15 | // Total size of entries and ZIP format overheads. 16 | Sizes total = 1; 17 | // Only dex code. 18 | Sizes dex = 2; 19 | // Resources, resource table and Android Manifest. 20 | Sizes resources = 3; 21 | // Only assets. 22 | Sizes assets = 4; 23 | // Only native libraries. 24 | Sizes native_libs = 5; 25 | // Other entries e.g. META-INF/ and ZIP format overheads. 26 | Sizes other = 6; 27 | } 28 | -------------------------------------------------------------------------------- /library/src/main/proto/targeting.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package android.bundle; 4 | 5 | import "google/protobuf/wrappers.proto"; 6 | 7 | option java_package = "com.android.bundle"; 8 | 9 | // Targeting on the level of variants. 10 | message VariantTargeting { 11 | SdkVersionTargeting sdk_version_targeting = 1; 12 | AbiTargeting abi_targeting = 2; 13 | ScreenDensityTargeting screen_density_targeting = 3; 14 | MultiAbiTargeting multi_abi_targeting = 4; 15 | TextureCompressionFormatTargeting texture_compression_format_targeting = 5; 16 | SdkRuntimeTargeting sdk_runtime_targeting = 6; 17 | } 18 | 19 | // Targeting on the level of individual APKs. 20 | message ApkTargeting { 21 | AbiTargeting abi_targeting = 1; 22 | reserved 2; // was GraphicsApiTargeting 23 | LanguageTargeting language_targeting = 3; 24 | ScreenDensityTargeting screen_density_targeting = 4; 25 | SdkVersionTargeting sdk_version_targeting = 5; 26 | TextureCompressionFormatTargeting texture_compression_format_targeting = 6; 27 | MultiAbiTargeting multi_abi_targeting = 7; 28 | SanitizerTargeting sanitizer_targeting = 8; 29 | DeviceTierTargeting device_tier_targeting = 9; 30 | CountrySetTargeting country_set_targeting = 10; 31 | } 32 | 33 | // Targeting on the module level. 34 | // The semantic of the targeting is the "AND" rule on all immediate values. 35 | message ModuleTargeting { 36 | SdkVersionTargeting sdk_version_targeting = 1; 37 | repeated DeviceFeatureTargeting device_feature_targeting = 2; 38 | UserCountriesTargeting user_countries_targeting = 3; 39 | DeviceGroupModuleTargeting device_group_targeting = 5; 40 | 41 | reserved 4; 42 | } 43 | 44 | // User Countries targeting describing an inclusive/exclusive list of country 45 | // codes that module targets. 46 | message UserCountriesTargeting { 47 | // List of country codes in the two-letter CLDR territory format. 48 | repeated string country_codes = 1; 49 | 50 | // Indicates if the list above is exclusive. 51 | bool exclude = 2; 52 | } 53 | 54 | message ScreenDensity { 55 | enum DensityAlias { 56 | DENSITY_UNSPECIFIED = 0; 57 | NODPI = 1; 58 | LDPI = 2; 59 | MDPI = 3; 60 | TVDPI = 4; 61 | HDPI = 5; 62 | XHDPI = 6; 63 | XXHDPI = 7; 64 | XXXHDPI = 8; 65 | } 66 | 67 | oneof density_oneof { 68 | DensityAlias density_alias = 1; 69 | int32 density_dpi = 2; 70 | } 71 | } 72 | 73 | message SdkVersion { 74 | // Inclusive. 75 | google.protobuf.Int32Value min = 1; 76 | } 77 | 78 | message TextureCompressionFormat { 79 | enum TextureCompressionFormatAlias { 80 | UNSPECIFIED_TEXTURE_COMPRESSION_FORMAT = 0; 81 | ETC1_RGB8 = 1; 82 | PALETTED = 2; 83 | THREE_DC = 3; 84 | ATC = 4; 85 | LATC = 5; 86 | DXT1 = 6; 87 | S3TC = 7; 88 | PVRTC = 8; 89 | ASTC = 9; 90 | ETC2 = 10; 91 | } 92 | TextureCompressionFormatAlias alias = 1; 93 | } 94 | 95 | message Abi { 96 | enum AbiAlias { 97 | UNSPECIFIED_CPU_ARCHITECTURE = 0; 98 | ARMEABI = 1; 99 | ARMEABI_V7A = 2; 100 | ARM64_V8A = 3; 101 | X86 = 4; 102 | X86_64 = 5; 103 | MIPS = 6; 104 | MIPS64 = 7; 105 | } 106 | AbiAlias alias = 1; 107 | } 108 | 109 | message MultiAbi { 110 | repeated Abi abi = 1; 111 | } 112 | 113 | message Sanitizer { 114 | enum SanitizerAlias { 115 | NONE = 0; 116 | HWADDRESS = 1; 117 | } 118 | SanitizerAlias alias = 1; 119 | } 120 | 121 | message DeviceFeature { 122 | string feature_name = 1; 123 | // Equivalent of android:glEsVersion or android:version in . 124 | int32 feature_version = 2; 125 | } 126 | 127 | // Targeting specific for directories under assets/. 128 | message AssetsDirectoryTargeting { 129 | AbiTargeting abi = 1; 130 | reserved 2; // was GraphicsApiTargeting 131 | TextureCompressionFormatTargeting texture_compression_format = 3; 132 | LanguageTargeting language = 4; 133 | DeviceTierTargeting device_tier = 5; 134 | CountrySetTargeting country_set = 6; 135 | } 136 | 137 | // Targeting specific for directories under lib/. 138 | message NativeDirectoryTargeting { 139 | Abi abi = 1; 140 | reserved 2; // was GraphicsApi 141 | TextureCompressionFormat texture_compression_format = 3; 142 | Sanitizer sanitizer = 4; 143 | } 144 | 145 | // Targeting specific for image files under apex/. 146 | message ApexImageTargeting { 147 | MultiAbiTargeting multi_abi = 1; 148 | } 149 | 150 | message AbiTargeting { 151 | repeated Abi value = 1; 152 | // Targeting of other sibling directories that were in the Bundle. 153 | // For master splits this is targeting of other master splits. 154 | repeated Abi alternatives = 2; 155 | } 156 | 157 | message MultiAbiTargeting { 158 | repeated MultiAbi value = 1; 159 | // Targeting of other sibling directories that were in the Bundle. 160 | // For master splits this is targeting of other master splits. 161 | repeated MultiAbi alternatives = 2; 162 | } 163 | 164 | message ScreenDensityTargeting { 165 | repeated ScreenDensity value = 1; 166 | // Targeting of other sibling directories that were in the Bundle. 167 | // For master splits this is targeting of other master splits. 168 | repeated ScreenDensity alternatives = 2; 169 | } 170 | 171 | message LanguageTargeting { 172 | // ISO-639: 2 or 3 letter language code. 173 | repeated string value = 1; 174 | // Targeting of other sibling directories that were in the Bundle. 175 | // For master splits this is targeting of other master splits. 176 | repeated string alternatives = 2; 177 | } 178 | 179 | message SdkVersionTargeting { 180 | repeated SdkVersion value = 1; 181 | // Targeting of other sibling directories that were in the Bundle. 182 | // For master splits this is targeting of other master splits. 183 | repeated SdkVersion alternatives = 2; 184 | } 185 | 186 | message TextureCompressionFormatTargeting { 187 | repeated TextureCompressionFormat value = 1; 188 | // Targeting of other sibling directories that were in the Bundle. 189 | // For master splits this is targeting of other master splits. 190 | repeated TextureCompressionFormat alternatives = 2; 191 | } 192 | 193 | message SanitizerTargeting { 194 | repeated Sanitizer value = 1; 195 | } 196 | 197 | // Since other atom targeting messages have the "OR" semantic on values 198 | // the DeviceFeatureTargeting represents only one device feature to retain 199 | // that convention. 200 | message DeviceFeatureTargeting { 201 | DeviceFeature required_feature = 1; 202 | } 203 | 204 | // Targets assets and APKs to a concrete device tier. 205 | message DeviceTierTargeting { 206 | repeated google.protobuf.Int32Value value = 3; 207 | repeated google.protobuf.Int32Value alternatives = 4; 208 | 209 | reserved 1, 2; 210 | } 211 | 212 | // Targets assets and APKs to a specific country set. 213 | // For Example:- 214 | // The values and alternatives for the following files in assets directory 215 | // targeting would be as follows: 216 | // assetpack1/assets/foo#countries_latam/bar.txt -> 217 | // { value: [latam], alternatives: [sea] } 218 | // assetpack1/assets/foo#countries_sea/bar.txt -> 219 | // { value: [sea], alternatives: [latam] } 220 | // assetpack1/assets/foo/bar.txt -> 221 | // { value: [], alternatives: [sea, latam] } 222 | // The values and alternatives for the following targeted split apks would be as 223 | // follows: 224 | // splits/base-countries_latam.apk -> 225 | // { value: [latam], alternatives: [sea] } 226 | // splits/base-countries_sea.apk -> 227 | // { value: [sea], alternatives: [latam] } 228 | // splits/base-other_countries.apk -> 229 | // { value: [], alternatives: [sea, latam] } 230 | message CountrySetTargeting { 231 | // Country set name defined in device tier config. 232 | repeated string value = 1; 233 | // Targeting of other sibling directories that were in the Bundle. 234 | repeated string alternatives = 2; 235 | } 236 | 237 | // Targets conditional modules to a set of device groups. 238 | message DeviceGroupModuleTargeting { 239 | repeated string value = 1; 240 | } 241 | 242 | // Variant targeting based on SDK Runtime availability on device. 243 | message SdkRuntimeTargeting { 244 | // Whether the variant requires SDK Runtime to be available on the device. 245 | bool requires_sdk_runtime = 1; 246 | } 247 | -------------------------------------------------------------------------------- /library/src/main/resources/vmsrc.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timscriptov/nmmp/ceb3417f756a4193dfe2ae24da27cf2a04ee1470/library/src/main/resources/vmsrc.zip -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "nmmp" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 7 | google() 8 | gradlePluginPortal() 9 | mavenCentral() 10 | } 11 | } 12 | 13 | dependencyResolutionManagement { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 18 | maven("https://www.jitpack.io") 19 | } 20 | } 21 | 22 | include(":composeApp") 23 | include(":library") 24 | --------------------------------------------------------------------------------