diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b331b52a..e7df7f12 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,7 @@
[versions]
agp = "8.1.2"
+apksig = "8.0.2"
+guava = "32.1.3-jre"
kotlin = "1.9.0"
kotlinx-coroutines-android = "1.7.3"
@@ -33,6 +35,7 @@ androidx-material-ripple = { module = "androidx.compose.material:material-ripple
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "ui-tooling-preview" }
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "ui-tooling-preview" }
+apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov-jdk18on" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" }
coil-video = { module = "io.coil-kt:coil-video", version.ref = "coil-compose" }
@@ -40,6 +43,7 @@ coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-andro
dexlib2 = { group = "org.smali", name = "dexlib2", version.ref = "dexlib2" }
ffmpeg-kit = { group = "com.arthenica", name = "ffmpeg-kit-full-gpl", version.ref = "ffmpeg-kit" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
+guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit = { module = "junit:junit", version.ref = "junit" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid-android" }
diff --git a/manager/.gitignore b/manager/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/manager/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts
new file mode 100644
index 00000000..9ceba5b0
--- /dev/null
+++ b/manager/build.gradle.kts
@@ -0,0 +1,83 @@
+import com.android.build.gradle.internal.api.BaseVariantOutputImpl
+
+plugins {
+ alias(libs.plugins.androidApplication)
+ alias(libs.plugins.kotlinAndroid)
+}
+
+android {
+ namespace = rootProject.ext["applicationId"].toString() + ".manager"
+ compileSdk = 34
+
+ androidResources {
+ noCompress += ".so"
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+
+ defaultConfig {
+ buildConfigField("String", "APPLICATION_ID", "\"${rootProject.ext["applicationId"]}\"")
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.2"
+ }
+
+ defaultConfig {
+ applicationId = rootProject.ext["applicationId"].toString() + ".manager"
+ versionCode = 1
+ versionName = "1.0.0"
+ minSdk = 28
+ targetSdk = 34
+ multiDexEnabled = true
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = true
+ proguardFiles += file("proguard-rules.pro")
+ }
+ debug {
+ isMinifyEnabled = false
+ }
+ }
+
+ applicationVariants.all {
+ outputs.map { it as BaseVariantOutputImpl }.forEach { outputVariant ->
+ outputVariant.outputFileName = "manager.apk"
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+configurations {
+ all {
+ resolutionStrategy {
+ exclude(group = "com.google.guava", module = "listenablefuture")
+ }
+ }
+}
+
+dependencies {
+ implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
+ implementation(libs.guava)
+ implementation(libs.apksig)
+ implementation(libs.gson)
+ implementation(libs.androidx.material3)
+ implementation(libs.androidx.activity.ktx)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.material.icons.core)
+ implementation(libs.androidx.material.ripple)
+ implementation(libs.androidx.material.icons.extended)
+}
\ No newline at end of file
diff --git a/manager/libs/ManifestEditor-1.0.2.jar b/manager/libs/ManifestEditor-1.0.2.jar
new file mode 100644
index 00000000..7dc59a7b
Binary files /dev/null and b/manager/libs/ManifestEditor-1.0.2.jar differ
diff --git a/manager/libs/apkzlib.jar b/manager/libs/apkzlib.jar
new file mode 100644
index 00000000..ca2dea2e
Binary files /dev/null and b/manager/libs/apkzlib.jar differ
diff --git a/manager/proguard-rules.pro b/manager/proguard-rules.pro
new file mode 100644
index 00000000..e4c7016b
--- /dev/null
+++ b/manager/proguard-rules.pro
@@ -0,0 +1,2 @@
+-dontwarn com.google.errorprone.annotations.**
+-dontwarn com.google.auto.value.**
\ No newline at end of file
diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..e0dbff13
--- /dev/null
+++ b/manager/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/manager/src/main/assets/lspatch/dexes/loader.dex b/manager/src/main/assets/lspatch/dexes/loader.dex
new file mode 100644
index 00000000..5e06ac44
Binary files /dev/null and b/manager/src/main/assets/lspatch/dexes/loader.dex differ
diff --git a/manager/src/main/assets/lspatch/dexes/metaloader.dex b/manager/src/main/assets/lspatch/dexes/metaloader.dex
new file mode 100644
index 00000000..57d0f47e
Binary files /dev/null and b/manager/src/main/assets/lspatch/dexes/metaloader.dex differ
diff --git a/manager/src/main/assets/lspatch/keystore.jks b/manager/src/main/assets/lspatch/keystore.jks
new file mode 100644
index 00000000..e5759c38
Binary files /dev/null and b/manager/src/main/assets/lspatch/keystore.jks differ
diff --git a/manager/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so b/manager/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so
new file mode 100644
index 00000000..b8e6a313
Binary files /dev/null and b/manager/src/main/assets/lspatch/so/arm64-v8a/liblspatch.so differ
diff --git a/manager/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so b/manager/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so
new file mode 100644
index 00000000..19add997
Binary files /dev/null and b/manager/src/main/assets/lspatch/so/armeabi-v7a/liblspatch.so differ
diff --git a/manager/src/main/assets/lspatch/version.txt b/manager/src/main/assets/lspatch/version.txt
new file mode 100644
index 00000000..5d4294b9
--- /dev/null
+++ b/manager/src/main/assets/lspatch/version.txt
@@ -0,0 +1 @@
+0.5.1
\ No newline at end of file
diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt
new file mode 100644
index 00000000..75c513ab
--- /dev/null
+++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt
@@ -0,0 +1,109 @@
+package me.rhunk.snapenhance.manager
+
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.border
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import me.rhunk.snapenhance.manager.lspatch.LSPatch
+import java.io.File
+import java.io.PrintWriter
+import java.io.StringWriter
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ val coroutineScope = rememberCoroutineScope()
+ MaterialTheme(
+ colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (isSystemInDarkTheme()) dynamicDarkColorScheme(LocalContext.current)
+ else dynamicLightColorScheme(LocalContext.current)
+ } else MaterialTheme.colorScheme
+ ) {
+ val context = LocalContext.current
+ val logs = remember { mutableStateListOf() }
+ fun printLog(data: Any) {
+ when (data) {
+ is Throwable -> {
+ logs += data.message.toString()
+ logs += StringWriter().apply {
+ data.printStackTrace(PrintWriter(this))
+ }.toString()
+ }
+ else -> logs += data.toString()
+ }
+ }
+
+ val scrollState = rememberLazyListState(0)
+
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(20.dp),
+ modifier = Modifier.padding(10.dp)
+ ) {
+ Text(text = "SE Manager")
+
+ Button(onClick = {
+ coroutineScope.launch(Dispatchers.IO) {
+ runCatching {
+ val lspatch = LSPatch(
+ context,
+ mapOf(
+ BuildConfig.APPLICATION_ID to File(context.packageManager.getPackageInfo(BuildConfig.APPLICATION_ID, 0).applicationInfo.sourceDir)
+ )
+ ) { printLog(it) }
+ lspatch.patch(
+ File(context.packageManager.getPackageInfo("com.snapchat.android", 0).applicationInfo.sourceDir),
+ File(context.filesDir, "patched.apk")
+ )
+ }.onFailure { printLog(it) }
+ }
+ }) {
+ Text(text = "Test patch apk")
+ }
+
+ LazyColumn(
+ state = scrollState,
+ modifier = Modifier.fillMaxWidth().padding(5.dp).height(500.dp).border(1.dp, color = Color.Black),
+ content = {
+ items(logs) {
+ Text(text = it, modifier = Modifier.padding(2.dp))
+ }
+ }
+ )
+
+ LaunchedEffect(logs.size) {
+ scrollState.scrollToItem((logs.size - 1).coerceAtLeast(0))
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/LSPatch.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/LSPatch.kt
new file mode 100644
index 00000000..b698cc3a
--- /dev/null
+++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/LSPatch.kt
@@ -0,0 +1,175 @@
+package me.rhunk.snapenhance.manager.lspatch
+
+import android.content.Context
+import com.android.tools.build.apkzlib.sign.SigningExtension
+import com.android.tools.build.apkzlib.sign.SigningOptions
+import com.android.tools.build.apkzlib.zip.AlignmentRules
+import com.android.tools.build.apkzlib.zip.ZFile
+import com.android.tools.build.apkzlib.zip.ZFileOptions
+import com.google.gson.Gson
+import com.wind.meditor.core.ManifestEditor
+import com.wind.meditor.property.AttributeItem
+import com.wind.meditor.property.ModificationProperty
+import me.rhunk.snapenhance.manager.lspatch.config.Constants.ORIGINAL_APK_ASSET_PATH
+import me.rhunk.snapenhance.manager.lspatch.config.Constants.PROXY_APP_COMPONENT_FACTORY
+import me.rhunk.snapenhance.manager.lspatch.config.PatchConfig
+import me.rhunk.snapenhance.manager.lspatch.util.ApkSignatureHelper
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.security.KeyStore
+import java.security.cert.X509Certificate
+import java.util.zip.ZipFile
+import kotlin.io.encoding.Base64
+import kotlin.io.encoding.ExperimentalEncodingApi
+
+
+//https://github.com/LSPosed/LSPatch/blob/master/patch/src/main/java/org/lsposed/patch/LSPatch.java
+class LSPatch(
+ private val context: Context,
+ private val modules: Map, //packageName -> file
+ private val printLog: (Any) -> Unit
+) {
+ companion object {
+ private val Z_FILE_OPTIONS = ZFileOptions().setAlignmentRule(
+ AlignmentRules.compose(
+ AlignmentRules.constantForSuffix(".so", 4096),
+ AlignmentRules.constantForSuffix(ORIGINAL_APK_ASSET_PATH, 4096)
+ )
+ )
+ }
+
+ private fun patchManifest(data: ByteArray, lspatchMetadata: String): ByteArray {
+ val property = ModificationProperty()
+
+ property.addApplicationAttribute(AttributeItem("appComponentFactory", PROXY_APP_COMPONENT_FACTORY))
+ property.addMetaData(ModificationProperty.MetaData("lspatch", lspatchMetadata))
+
+ return ByteArrayOutputStream().apply {
+ ManifestEditor(ByteArrayInputStream(data), this, property).processManifest()
+ flush()
+ close()
+ }.toByteArray()
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @OptIn(ExperimentalEncodingApi::class)
+ private fun patchApk(inputApkFile: File, outputFile: File) {
+ printLog("Patching ${inputApkFile.absolutePath} to ${outputFile.absolutePath}")
+ val dstZFile = ZFile.openReadWrite(outputFile, Z_FILE_OPTIONS)
+ val sourceApkFile = dstZFile.addNestedZip({ ORIGINAL_APK_ASSET_PATH }, inputApkFile, false)
+
+ val patchConfig = PatchConfig(
+ useManager = false,
+ debuggable = false,
+ overrideVersionCode = false,
+ sigBypassLevel = 2,
+ originalSignature = ApkSignatureHelper.getApkSignInfo(inputApkFile.absolutePath),
+ appComponentFactory = "androidx.core.app.CoreComponentFactory"
+ ).let { Gson().toJson(it) }
+
+ // sign apk
+ runCatching {
+ val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
+ keyStore.load(context.assets.open("lspatch/keystore.jks"), "android".toCharArray())
+ val key = keyStore.getEntry("androiddebugkey", KeyStore.PasswordProtection("android".toCharArray())) as KeyStore.PrivateKeyEntry
+ SigningExtension(
+ SigningOptions.builder().apply {
+ setMinSdkVersion(28)
+ setV2SigningEnabled(true)
+ setCertificates(*(key.certificateChain as Array))
+ setKey(key.privateKey)
+ }.build()
+ ).apply {
+ register(dstZFile)
+ }
+ }.onFailure {
+ throw Exception("Failed to sign apk", it)
+ }
+
+ printLog("Patching manifest")
+
+ val originalManifestEntry = sourceApkFile.get("AndroidManifest.xml") ?: throw Exception("No original manifest found")
+ originalManifestEntry.open().use { inputStream ->
+ val patchedManifestData = patchManifest(inputStream.readBytes(), Base64.encode(patchConfig.toByteArray()))
+ dstZFile.add("AndroidManifest.xml", patchedManifestData.inputStream())
+ }
+
+ //add config
+ printLog("Adding config")
+ dstZFile.add("assets/lspatch/config.json", ByteArrayInputStream(patchConfig.toByteArray()))
+
+
+ // add loader dex
+ printLog("Adding dex files")
+ dstZFile.add("classes.dex", context.assets.open("lspatch/dexes/metaloader.dex"))
+ dstZFile.add("assets/lspatch/loader.dex", context.assets.open("lspatch/dexes/loader.dex"))
+
+ //add natives
+ printLog("Adding natives")
+ context.assets.list("lspatch/so")?.forEach { native ->
+ dstZFile.add("assets/lspatch/so/$native/liblspatch.so", context.assets.open("lspatch/so/$native/liblspatch.so"), false)
+ }
+
+
+ //embed modules
+ printLog("embedding modules")
+ modules.forEach { (packageName, module) ->
+ dstZFile.add("assets/lspatch/modules/$packageName.apk", module.inputStream())
+ }
+
+
+ // link apk entries
+ printLog("Linking apk entries")
+
+ for (entry in sourceApkFile.entries()) {
+ val name = entry.centralDirectoryHeader.name
+ if (name.startsWith("classes") && name.endsWith(".dex")) continue
+ if (dstZFile[name] != null) continue
+ if (name == "AndroidManifest.xml") continue
+ if (name.startsWith("META-INF") && (name.endsWith(".SF") || name.endsWith(".MF") || name.endsWith(
+ ".RSA"
+ ))
+ ) continue
+ sourceApkFile.addFileLink(name, name)
+ }
+
+ dstZFile.realign()
+ dstZFile.close()
+ printLog("Done")
+ }
+
+ fun patch(input: File, outputFile: File) {
+ //check if input apk is already patched
+ var isAlreadyPatched = false
+ var inputFile = input
+
+ // extract origin
+ printLog("Extracting origin apk")
+ ZipFile(input).use { zipFile ->
+ zipFile.getEntry("assets/lspatch/origin.apk")?.apply {
+ inputFile = File.createTempFile("origin", ".apk")
+ inputFile.outputStream().use {
+ zipFile.getInputStream(this).copyTo(it)
+ }
+ isAlreadyPatched = true
+ }
+ }
+
+ if (outputFile.exists()) outputFile.delete()
+
+ printLog("Patching apk")
+ runCatching {
+ patchApk(inputFile, outputFile)
+ }.onFailure {
+ if (isAlreadyPatched) {
+ inputFile.delete()
+ }
+ outputFile.delete()
+ printLog("Failed to patch")
+ printLog(it)
+ }.onSuccess {
+ outputFile.delete()
+ }
+ }
+}
\ No newline at end of file
diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/config/Constants.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/config/Constants.kt
new file mode 100644
index 00000000..0af8c9de
--- /dev/null
+++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/config/Constants.kt
@@ -0,0 +1,8 @@
+package me.rhunk.snapenhance.manager.lspatch.config
+
+//https://github.com/LSPosed/LSPatch/blob/master/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java
+object Constants {
+ const val ORIGINAL_APK_ASSET_PATH = "assets/lspatch/origin.apk"
+ const val PROXY_APP_COMPONENT_FACTORY =
+ "org.lsposed.lspatch.metaloader.LSPAppComponentFactoryStub"
+}
\ No newline at end of file
diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/config/PatchConfig.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/config/PatchConfig.kt
new file mode 100644
index 00000000..358fe02d
--- /dev/null
+++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/config/PatchConfig.kt
@@ -0,0 +1,19 @@
+package me.rhunk.snapenhance.manager.lspatch.config
+
+data class PatchConfig(
+ val useManager: Boolean = false,
+ val debuggable: Boolean = false,
+ val overrideVersionCode: Boolean = false,
+ val sigBypassLevel: Int = 0,
+ val originalSignature: String? = null,
+ val appComponentFactory: String? = null,
+ val lspConfig: LSPConfig? = LSPConfig()
+) {
+ data class LSPConfig(
+ var API_CODE: Int = 93,
+ var VERSION_CODE: Int = 360,
+ var VERSION_NAME: String = "0.5.1",
+ var CORE_VERSION_CODE: Int = 6649,
+ var CORE_VERSION_NAME: String = "1.8.5",
+ )
+}
\ No newline at end of file
diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/util/ApkSignatureHelper.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/util/ApkSignatureHelper.kt
new file mode 100644
index 00000000..a1c628fe
--- /dev/null
+++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/lspatch/util/ApkSignatureHelper.kt
@@ -0,0 +1,146 @@
+package me.rhunk.snapenhance.manager.lspatch.util;
+
+import java.io.IOException
+import java.io.RandomAccessFile
+import java.io.UnsupportedEncodingException
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.security.cert.Certificate
+import java.util.Enumeration
+import java.util.jar.JarEntry
+import java.util.jar.JarFile
+
+
+//https://github.com/LSPosed/LSPatch/blob/master/patch/src/main/java/org/lsposed/patch/util/ApkSignatureHelper.java
+object ApkSignatureHelper {
+ private val APK_V2_MAGIC = charArrayOf('A', 'P', 'K', ' ', 'S', 'i', 'g', ' ',
+ 'B', 'l', 'o', 'c', 'k', ' ', '4', '2')
+
+ private fun toChars(mSignature: ByteArray): CharArray {
+ val N = mSignature.size
+ val N2 = N * 2
+ val text = CharArray(N2)
+ for (j in 0 until N) {
+ val v = mSignature[j]
+ var d = v.toInt() shr 4 and 0xf
+ text[j * 2] = (if (d >= 10) 'a'.code + d - 10 else '0'.code + d).toChar()
+ d = v.toInt() and 0xf
+ text[j * 2 + 1] = (if (d >= 10) 'a'.code + d - 10 else '0'.code + d).toChar()
+ }
+ return text
+ }
+
+ private fun loadCertificates(
+ jarFile: JarFile,
+ je: JarEntry?,
+ readBuffer: ByteArray
+ ): Array? {
+ try {
+ val `is` = jarFile.getInputStream(je)
+ while (`is`.read(readBuffer, 0, readBuffer.size) != -1) {
+ }
+ `is`.close()
+ return je?.certificates as Array?
+ } catch (e: Exception) {
+ }
+ return null
+ }
+
+ fun getApkSignInfo(apkFilePath: String): String? {
+ return try {
+ getApkSignV2(apkFilePath)
+ } catch (e: Exception) {
+ getApkSignV1(apkFilePath)
+ }
+ }
+
+ fun getApkSignV1(apkFilePath: String?): String? {
+ val readBuffer = ByteArray(8192)
+ var certs: Array? = null
+ try {
+ val jarFile = JarFile(apkFilePath)
+ val entries: Enumeration<*> = jarFile.entries()
+ while (entries.hasMoreElements()) {
+ val je = entries.nextElement() as JarEntry
+ if (je.isDirectory) {
+ continue
+ }
+ if (je.name.startsWith("META-INF/")) {
+ continue
+ }
+ val localCerts = loadCertificates(jarFile, je, readBuffer)
+ if (certs == null) {
+ certs = localCerts
+ } else {
+ for (i in certs.indices) {
+ var found = false
+ for (j in localCerts!!.indices) {
+ if (certs[i] != null && certs[i] == localCerts[j]) {
+ found = true
+ break
+ }
+ }
+ if (!found || certs.size != localCerts.size) {
+ jarFile.close()
+ return null
+ }
+ }
+ }
+ }
+ jarFile.close()
+ return if (certs != null) String(toChars(certs[0]!!.encoded)) else null
+ } catch (ignored: Throwable) {
+ }
+ return null
+ }
+
+ @Throws(IOException::class)
+ private fun getApkSignV2(apkFilePath: String): String {
+ RandomAccessFile(apkFilePath, "r").use { apk ->
+ val buffer = ByteBuffer.allocate(0x10)
+ buffer.order(ByteOrder.LITTLE_ENDIAN)
+ apk.seek(apk.length() - 0x6)
+ apk.readFully(buffer.array(), 0x0, 0x6)
+ val offset = buffer.getInt()
+ if (buffer.getShort().toInt() != 0) {
+ throw UnsupportedEncodingException("no zip")
+ }
+ apk.seek((offset - 0x10).toLong())
+ apk.readFully(buffer.array(), 0x0, 0x10)
+ if (!buffer.array().contentEquals(APK_V2_MAGIC.map { it.code.toByte() }.toByteArray())) {
+ throw UnsupportedEncodingException("no apk v2")
+ }
+
+ // Read and compare size fields
+ apk.seek((offset - 0x18).toLong())
+ apk.readFully(buffer.array(), 0x0, 0x8)
+ buffer.rewind()
+ var size = buffer.getLong().toInt()
+ val block = ByteBuffer.allocate(size + 0x8)
+ block.order(ByteOrder.LITTLE_ENDIAN)
+ apk.seek((offset - block.capacity()).toLong())
+ apk.readFully(block.array(), 0x0, block.capacity())
+ if (size.toLong() != block.getLong()) {
+ throw UnsupportedEncodingException("no apk v2")
+ }
+ while (block.remaining() > 24) {
+ size = block.getLong().toInt()
+ if (block.getInt() == 0x7109871a) {
+ // signer-sequence length, signer length, signed data length
+ block.position(block.position() + 12)
+ size = block.getInt() // digests-sequence length
+
+ // digests, certificates length
+ block.position(block.position() + size + 0x4)
+ size = block.getInt() // certificate length
+ break
+ } else {
+ block.position(block.position() + size - 0x4)
+ }
+ }
+ val certificate = ByteArray(size)
+ block[certificate]
+ return String(toChars(certificate))
+ }
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index e0b7738c..9adbce0f 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -21,4 +21,5 @@ include(":common")
include(":core")
include(":app")
include(":mapper")
-include(":native")
\ No newline at end of file
+include(":native")
+include(":manager")
\ No newline at end of file