Skip to content

Commit 775dc66

Browse files
committed
Add new tests for app hiding
1 parent 47f4739 commit 775dc66

File tree

14 files changed

+262
-162
lines changed

14 files changed

+262
-162
lines changed

app/core/proguard-rules.pro

+1-10
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,4 @@
3737
-flattenpackagehierarchy
3838
-allowaccessmodification
3939

40-
-dontwarn org.junit.Assert
41-
-dontwarn org.bouncycastle.jsse.BCSSLParameters
42-
-dontwarn org.bouncycastle.jsse.BCSSLSocket
43-
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
44-
-dontwarn org.commonmark.ext.gfm.strikethrough.Strikethrough
45-
-dontwarn org.conscrypt.Conscrypt*
46-
-dontwarn org.conscrypt.ConscryptHostnameVerifier
47-
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
48-
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
49-
-dontwarn org.openjsse.net.ssl.OpenJSSE
40+
-dontwarn org.junit.**

app/core/src/main/java/com/topjohnwu/magisk/core/TestImpl.kt

-50
This file was deleted.

app/core/src/main/java/com/topjohnwu/magisk/core/su/SuRequestHandler.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class SuRequestHandler(
6262
return false
6363
}
6464
output = File(fifo)
65-
policy = SuPolicy(uid)
65+
policy = policyDB.fetch(uid) ?: SuPolicy(uid)
6666
try {
6767
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
6868
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()

app/core/src/main/java/com/topjohnwu/magisk/core/tasks/AppMigration.kt

+78-34
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.app.Activity
44
import android.app.ActivityOptions
55
import android.content.Context
66
import android.content.Intent
7+
import android.content.pm.PackageManager
78
import android.os.Build
89
import android.widget.Toast
910
import com.topjohnwu.magisk.StubApk
@@ -25,7 +26,6 @@ import kotlinx.coroutines.Dispatchers
2526
import kotlinx.coroutines.withContext
2627
import timber.log.Timber
2728
import java.io.File
28-
import java.io.FileOutputStream
2929
import java.io.IOException
3030
import java.io.OutputStream
3131
import java.security.SecureRandom
@@ -36,6 +36,7 @@ object AppMigration {
3636
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
3737
private const val ALPHADOTS = "$ALPHA....."
3838
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
39+
private const val TEST_PKG_NAME = "$APP_PACKAGE_NAME.test"
3940

4041
// Some arbitrary limit
4142
const val MAX_LABEL_LENGTH = 32
@@ -131,21 +132,15 @@ object AppMigration {
131132
val je = jar.getJarEntry(ANDROID_MANIFEST)
132133
val xml = AXML(jar.getRawData(je))
133134
val generator = classNameGenerator()
134-
135-
if (!xml.patchStrings {
136-
for (i in it.indices) {
137-
val s = it[i]
138-
if (s.contains(APP_PACKAGE_NAME)) {
139-
it[i] = s.replace(APP_PACKAGE_NAME, pkg)
140-
} else if (s.contains(PLACEHOLDER)) {
141-
it[i] = generator.next()
142-
} else if (s == origLabel) {
143-
it[i] = label.toString()
144-
}
135+
val p = xml.patchStrings {
136+
when {
137+
it.contains(APP_PACKAGE_NAME) -> it.replace(APP_PACKAGE_NAME, pkg)
138+
it.contains(PLACEHOLDER) -> generator.next()
139+
it == origLabel -> label.toString()
140+
else -> it
145141
}
146-
}) {
147-
return false
148142
}
143+
if (!p) return false
149144

150145
// Write apk changes
151146
jar.getOutputStream(je).use { it.write(xml.bytes) }
@@ -159,40 +154,83 @@ object AppMigration {
159154
}
160155
}
161156

162-
private fun launchApp(activity: Activity, pkg: String) {
163-
val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
157+
private fun patchTest(apk: File, out: File, pkg: String): Boolean {
158+
try {
159+
JarMap.open(apk, true).use { jar ->
160+
val je = jar.getJarEntry(ANDROID_MANIFEST)
161+
val xml = AXML(jar.getRawData(je))
162+
val p = xml.patchStrings {
163+
when (it) {
164+
APP_PACKAGE_NAME -> pkg
165+
TEST_PKG_NAME -> "$pkg.test"
166+
else -> it
167+
}
168+
}
169+
if (!p) return false
170+
171+
// Write apk changes
172+
jar.getOutputStream(je).use { it.write(xml.bytes) }
173+
val keys = Keygen()
174+
out.outputStream().use { SignApk.sign(keys.cert, keys.key, jar, it) }
175+
return true
176+
}
177+
} catch (e: Exception) {
178+
Timber.e(e)
179+
return false
180+
}
181+
}
182+
183+
private fun launchApp(context: Context, pkg: String) {
184+
val intent = context.packageManager.getLaunchIntentForPackage(pkg) ?: return
164185
intent.putExtra(Const.Key.PREV_CONFIG, Config.toBundle())
165186
val options = ActivityOptions.makeBasic()
166187
if (Build.VERSION.SDK_INT >= 34) {
167188
options.setShareIdentityEnabled(true)
168189
}
169-
activity.startActivity(intent, options.toBundle())
170-
activity.finish()
190+
context.startActivity(intent, options.toBundle())
191+
if (context is Activity) {
192+
context.finish()
193+
}
171194
}
172195

173-
private suspend fun patchAndHide(activity: Activity, label: String): Boolean {
174-
val stub = File(activity.cacheDir, "stub.apk")
196+
suspend fun patchAndHide(context: Context, label: String, pkg: String? = null): Boolean {
197+
val stub = File(context.cacheDir, "stub.apk")
175198
try {
176-
activity.assets.open("stub.apk").writeTo(stub)
199+
context.assets.open("stub.apk").writeTo(stub)
177200
} catch (e: IOException) {
178201
Timber.e(e)
179202
return false
180203
}
181204

182-
// Generate a new random package name and signature
183-
val repack = File(activity.cacheDir, "patched.apk")
184-
val pkg = genPackageName()
205+
// Generate a new random signature and package name if needed
206+
val pkg = pkg ?: genPackageName()
185207
Config.keyStoreRaw = ""
186208

187-
if (!patch(activity, stub, FileOutputStream(repack), pkg, label))
188-
return false
209+
// Check and patch the test APK
210+
try {
211+
val info = context.packageManager.getApplicationInfo(TEST_PKG_NAME, 0)
212+
val testApk = File(info.sourceDir)
213+
val testRepack = File(context.cacheDir, "test.apk")
214+
if (!patchTest(testApk, testRepack, pkg))
215+
return false
216+
val cmd = "adb_pm_install $testRepack $pkg.test"
217+
if (!Shell.cmd(cmd).exec().isSuccess)
218+
return false
219+
} catch (e: PackageManager.NameNotFoundException) {
220+
}
221+
222+
val repack = File(context.cacheDir, "patched.apk")
223+
repack.outputStream().use {
224+
if (!patch(context, stub, it, pkg, label))
225+
return false
226+
}
189227

190228
// Install and auto launch app
191229
val cmd = "adb_pm_install $repack $pkg"
192230
if (Shell.cmd(cmd).exec().isSuccess) {
193231
Config.suManager = pkg
194232
Shell.cmd("touch $AppApkPath").exec()
195-
launchApp(activity, pkg)
233+
launchApp(context, pkg)
196234
return true
197235
} else {
198236
return false
@@ -216,6 +254,18 @@ object AppMigration {
216254
}
217255
}
218256

257+
suspend fun restoreApp(context: Context): Boolean {
258+
val apk = StubApk.current(context)
259+
val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME"
260+
if (Shell.cmd(cmd).await().isSuccess) {
261+
Config.suManager = ""
262+
Shell.cmd("touch $AppApkPath").exec()
263+
launchApp(context, APP_PACKAGE_NAME)
264+
return true
265+
}
266+
return false
267+
}
268+
219269
@Suppress("DEPRECATION")
220270
suspend fun restore(activity: Activity) {
221271
val dialog = android.app.ProgressDialog(activity).apply {
@@ -224,13 +274,7 @@ object AppMigration {
224274
setCancelable(false)
225275
show()
226276
}
227-
val apk = StubApk.current(activity)
228-
val cmd = "adb_pm_install $apk $APP_PACKAGE_NAME"
229-
if (Shell.cmd(cmd).await().isSuccess) {
230-
Config.suManager = ""
231-
Shell.cmd("touch $AppApkPath").exec()
232-
launchApp(activity, APP_PACKAGE_NAME)
233-
} else {
277+
if (!restoreApp(activity)) {
234278
activity.toast(R.string.failure, Toast.LENGTH_LONG)
235279
}
236280
dialog.dismiss()

app/core/src/main/java/com/topjohnwu/magisk/core/utils/AXML.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class AXML(b: ByteArray) {
2929
* Followed by an array of uint32_t with size = number of strings
3030
* Each entry points to an offset into the string data
3131
*/
32-
fun patchStrings(patchFn: (Array<String>) -> Unit): Boolean {
32+
fun patchStrings(mapFn: (String) -> String): Boolean {
3333
val buffer = ByteBuffer.wrap(bytes).order(LITTLE_ENDIAN)
3434

3535
fun findStringPool(): Int {
@@ -65,7 +65,9 @@ class AXML(b: ByteArray) {
6565
}
6666

6767
val strArr = strList.toTypedArray()
68-
patchFn(strArr)
68+
for (i in strArr.indices) {
69+
strArr[i] = mapFn(strArr[i])
70+
}
6971

7072
// Write everything before string data, will patch values later
7173
val baos = RawByteStream()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.topjohnwu.magisk.test
2+
3+
import androidx.annotation.Keep
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import androidx.test.platform.app.InstrumentationRegistry
6+
import com.topjohnwu.magisk.core.BuildConfig.APP_PACKAGE_NAME
7+
import com.topjohnwu.magisk.core.Config
8+
import com.topjohnwu.magisk.core.di.ServiceLocator
9+
import com.topjohnwu.magisk.core.model.su.SuPolicy
10+
import com.topjohnwu.magisk.core.tasks.AppMigration
11+
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
12+
import com.topjohnwu.superuser.CallbackList
13+
import kotlinx.coroutines.runBlocking
14+
import org.junit.Assert.assertTrue
15+
import org.junit.BeforeClass
16+
import org.junit.Test
17+
import org.junit.runner.RunWith
18+
import timber.log.Timber
19+
20+
@Keep
21+
@RunWith(AndroidJUnit4::class)
22+
class Environment {
23+
24+
companion object {
25+
@BeforeClass
26+
@JvmStatic
27+
fun before() = MagiskAppTest.before()
28+
}
29+
30+
@Test
31+
fun setupMagisk() {
32+
val log = object : CallbackList<String>(Runnable::run) {
33+
override fun onAddElement(e: String) {
34+
Timber.i(e)
35+
}
36+
}
37+
runBlocking {
38+
assertTrue(
39+
"Magisk setup failed",
40+
MagiskInstaller.Emulator(log, log).exec()
41+
)
42+
}
43+
}
44+
45+
@Test
46+
fun setupShellGrantTest() {
47+
runBlocking {
48+
// Inject an undetermined policy for ADB shell
49+
val policy = SuPolicy(2000)
50+
// Mute logging and notifications
51+
policy.logging = false
52+
policy.notification = false
53+
ServiceLocator.policyDB.update(policy)
54+
// Bypass the need to actually show a dialog
55+
Config.suAutoResponse = Config.Value.SU_AUTO_ALLOW
56+
Config.prefs.edit().commit()
57+
}
58+
}
59+
60+
@Test
61+
fun setupAppHide() {
62+
runBlocking {
63+
assertTrue(
64+
"App hiding failed",
65+
AppMigration.patchAndHide(
66+
context = InstrumentationRegistry.getInstrumentation().targetContext,
67+
label = "Settings",
68+
pkg = "repackaged.$APP_PACKAGE_NAME"
69+
)
70+
)
71+
}
72+
}
73+
74+
@Test
75+
fun setupAppRestore() {
76+
runBlocking {
77+
assertTrue(
78+
"App restoration failed",
79+
AppMigration.restoreApp(
80+
context = InstrumentationRegistry.getInstrumentation().targetContext
81+
)
82+
)
83+
}
84+
}
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.topjohnwu.magisk.test
2+
3+
import androidx.annotation.Keep
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import com.topjohnwu.magisk.core.Info
6+
import com.topjohnwu.magisk.core.utils.RootUtils
7+
import com.topjohnwu.superuser.Shell
8+
import org.junit.Assert.assertTrue
9+
import org.junit.BeforeClass
10+
import org.junit.Test
11+
import org.junit.runner.RunWith
12+
13+
@Keep
14+
@RunWith(AndroidJUnit4::class)
15+
class MagiskAppTest {
16+
17+
companion object {
18+
@BeforeClass
19+
@JvmStatic
20+
fun before() {
21+
assertTrue("Should have root access", Shell.getShell().isRoot)
22+
// Make sure the root service is running
23+
RootUtils.Connection.await()
24+
}
25+
}
26+
27+
@Test
28+
fun testZygisk() {
29+
assertTrue("Zygisk should be enabled", Info.isZygiskEnabled)
30+
}
31+
}

0 commit comments

Comments
 (0)