From 62324b3c78c911c0b98ccc48ab409559758fdb2f Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Fri, 21 Apr 2023 03:01:04 -0400 Subject: [PATCH] Work around READ_CALL_LOG being hard-restricted Since Android 10, READ_CALL_LOG has been marked as a hard-restricted permission, which prevents it from being granted by the user unless the app is given an exemption. There are three types of exemptions: system, upgrade, and installer. System exemptions can be given by `/system/etc/default-permissions` (only on first boot) or via roles. Neither are usable for BCR. Upgrade exemptions are only given during Android OS upgrades that further restrict existing permissions. Installer exemptions are given by whichever app installs another app, but there's no installer when it comes to adding new system apps, like BCR. Since AOSP has no sane built-in way to exempt BCR from the hard restriction, we'll do it ourselves. This commit introduces a new CLI utility baked in BCR that will talk to PermissionManager (Android 11+) or PackageManager (Android 10) to adjust its permission flags. This will unfortunately require two flashes (but only one reboot) for the initial install, because BCR must have already been loaded by the package manager for the flags to be changed. However, once the exemption has been granted, it persists across future upgrades. Fixes: #304 Signed-off-by: Andrew Gunnerson --- README.md | 8 + app/build.gradle.kts | 2 + app/magisk/customize.sh | 24 ++ app/proguard-rules.pro | 5 + .../bcr/standalone/RemoveHardRestrictions.kt | 257 ++++++++++++++++++ 5 files changed, 296 insertions(+) create mode 100644 app/magisk/customize.sh create mode 100644 app/src/main/java/com/chiller3/bcr/standalone/RemoveHardRestrictions.kt diff --git a/README.md b/README.md index 5daf7d4e7..c34e08949 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,14 @@ As the name alludes, BCR intends to be a basic as possible. The project will hav * **For OnePlus and Realme devices running the stock firmware (or custom firmware based on the stock firmware)**, also extract the `.apk` from the zip and install it manually before rebooting. This is necessary to work around a bug in the firmware where the app data directory does not get created, causing BCR to open up to a blank screen. * **For unrooted custom firmware**, flash the zip while booted into recovery. + * **NOTE**: The `READ_CALL_LOG` permission is hard restricted in Android 10+, which prevents it from being granted, even via Android's settings. To remove this restriction, run via adb: + ```bash + # If rooted, run inside of `su`: + CLASSPATH=/system/priv-app/com.chiller3.bcr/app-release.apk app_process / com.chiller3.bcr.standalone.RemoveHardRestrictionsKt + + # If unrooted, install BCR as both a user app and a system app: + pm install /system/priv-app/com.chiller3.bcr/app-release.apk + ``` * **NOTE**: If the custom firmware's `system` partition is formatted with `erofs`, then the filesystem is read-only and it is not possible to use this method. * Manually extracting the files from the `system/` folder in the zip will also work as long as the files have `644` permissions and the `u:object_r:system_file:s0` SELinux label. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 86a10294b..30c04a6ee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -350,6 +350,8 @@ android.applicationVariants.all { } } + from(File(magiskDir, "customize.sh")) + from(File(rootDir, "LICENSE")) from(File(rootDir, "README.md")) } diff --git a/app/magisk/customize.sh b/app/magisk/customize.sh new file mode 100644 index 000000000..725249555 --- /dev/null +++ b/app/magisk/customize.sh @@ -0,0 +1,24 @@ +# READ_CALL_LOG is a hard-restricted permission in Android 10+. It cannot be +# granted by the user unless it is exempted by the system. The most common way +# to do this is via the installer, but that's not applicable when adding new +# system apps. Instead, we talk to the permission service directly over binder +# to alter the flags. This requires flashing a second time after a reboot so +# that the package manager is already aware of BCR. + +app_id=$(grep '^id=' "${MODPATH}/module.prop" | cut -d= -f2) + +CLASSPATH=$(find "${MODPATH}"/system/priv-app/"${app_id}" -name '*.apk') \ + app_process \ + / \ + com.chiller3.bcr.standalone.RemoveHardRestrictionsKt \ + 2>&1 + +case "${?}" in +0|2) + exit 0 + ;; +*) + rm -rv "${MODPATH}" 2>&1 + exit 1 + ;; +esac diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e396b8a1b..a2da46eaf 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -29,3 +29,8 @@ -keepclassmembers class androidx.documentfile.provider.TreeDocumentFile { (androidx.documentfile.provider.DocumentFile, android.content.Context, android.net.Uri); } + +# Keep standalone CLI utilities +-keep class com.chiller3.bcr.standalone.* { + *; +} \ No newline at end of file diff --git a/app/src/main/java/com/chiller3/bcr/standalone/RemoveHardRestrictions.kt b/app/src/main/java/com/chiller3/bcr/standalone/RemoveHardRestrictions.kt new file mode 100644 index 000000000..3edc262a0 --- /dev/null +++ b/app/src/main/java/com/chiller3/bcr/standalone/RemoveHardRestrictions.kt @@ -0,0 +1,257 @@ +@file:SuppressLint( + "BlockedPrivateApi", + "DiscouragedPrivateApi", + "PrivateApi", + "SoonBlockedPrivateApi", +) + +package com.chiller3.bcr.standalone + +import android.Manifest +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.os.Build +import android.os.Process +import android.system.ErrnoException +import androidx.annotation.RequiresApi +import com.chiller3.bcr.BuildConfig +import kotlin.system.exitProcess + +private object ActivityThreadProxy { + private val CLS = Class.forName("android.app.ActivityThread") + private val METHOD_GET_PACKAGE_MANAGER = CLS.getDeclaredMethod("getPackageManager") + private val METHOD_GET_PERMISSION_MANAGER = CLS.getDeclaredMethod("getPermissionManager") + + fun getPackageManager(): PackageManagerProxy { + val iface = METHOD_GET_PACKAGE_MANAGER.invoke(null)!! + return PackageManagerProxy(iface) + } + + @RequiresApi(Build.VERSION_CODES.R) + fun getPermissionManager(): PermissionManagerProxy { + val iface = METHOD_GET_PERMISSION_MANAGER.invoke(null)!! + return PermissionManagerProxy(iface) + } +} + +private class PackageManagerProxy(private val iface: Any) { + companion object { + private val CLS = Class.forName("android.content.pm.IPackageManager") + private val METHOD_IS_PACKAGE_AVAILABLE = CLS.getDeclaredMethod( + "isPackageAvailable", String::class.java, Int::class.java) + // Android 10 only + private val METHOD_GET_PERMISSION_FLAGS by lazy { + CLS.getDeclaredMethod( + "getPermissionFlags", + String::class.java, + String::class.java, + Int::class.java, + ) + } + // Android 10 only + private val METHOD_UPDATE_PERMISSION_FLAGS by lazy { + CLS.getDeclaredMethod( + "updatePermissionFlags", + String::class.java, + String::class.java, + Int::class.java, + Int::class.java, + Int::class.java, + ) + } + + private val WRAPPER_CLS = PackageManager::class.java + val FLAG_PERMISSION_APPLY_RESTRICTION = WRAPPER_CLS.getDeclaredField( + "FLAG_PERMISSION_APPLY_RESTRICTION").getInt(null) + val FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT = WRAPPER_CLS.getDeclaredField( + "FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT").getInt(null) + val FLAG_PERMISSION_RESTRICTION_ANY_EXEMPT = WRAPPER_CLS.getDeclaredField( + "FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT").getInt(null) + } + + fun isPackageAvailable(packageName: String, userId: Int): Boolean { + return METHOD_IS_PACKAGE_AVAILABLE.invoke(iface, packageName, userId) as Boolean + } + + fun getPermissionFlags(permissionName: String, packageName: String, userId: Int): Int { + return METHOD_GET_PERMISSION_FLAGS.invoke(iface, permissionName, packageName, userId) as Int + } + + fun updatePermissionFlags( + permissionName: String, + packageName: String, + flagMask: Int, + flagValues: Int, + userId: Int, + ) { + METHOD_UPDATE_PERMISSION_FLAGS.invoke( + iface, + permissionName, + packageName, + flagMask, + flagValues, + userId, + ) + } + +} + +@RequiresApi(Build.VERSION_CODES.R) +private class PermissionManagerProxy(private val iface: Any) { + companion object { + private val CLS = Class.forName("android.permission.IPermissionManager") + private val METHOD_GET_PERMISSION_FLAGS = + CLS.getDeclaredMethod( + "getPermissionFlags", + String::class.java, + String::class.java, + Int::class.java, + ) + private val METHOD_UPDATE_PERMISSION_FLAGS = + CLS.getDeclaredMethod( + "updatePermissionFlags", + String::class.java, + String::class.java, + Int::class.java, + Int::class.java, + Boolean::class.java, + Int::class.java, + ) + } + + fun getPermissionFlags(packageName: String, permissionName: String, userId: Int): Int { + return METHOD_GET_PERMISSION_FLAGS(iface, packageName, permissionName, userId) as Int + } + + fun updatePermissionFlags( + packageName: String, + permissionName: String, + flagMask: Int, + flagValues: Int, + checkAdjustPolicyFlagPermission: Boolean, + userId: Int, + ) { + METHOD_UPDATE_PERMISSION_FLAGS.invoke( + iface, + packageName, + permissionName, + flagMask, + flagValues, + checkAdjustPolicyFlagPermission, + userId, + ) + } +} + +private fun switchToSystemUid() { + if (Process.myUid() != Process.SYSTEM_UID) { + val setUid = Process::class.java.getDeclaredMethod("setUid", Int::class.java) + val errno = setUid.invoke(null, Process.SYSTEM_UID) as Int + + if (errno != 0) { + throw Exception("Failed to switch to SYSTEM (${Process.SYSTEM_UID}) user", + ErrnoException("setuid", errno)) + } + if (Process.myUid() != Process.SYSTEM_UID) { + throw IllegalStateException("UID didn't actually change: " + + "${Process.myUid()} != ${Process.SYSTEM_UID}") + } + } +} + +@Suppress("SameParameterValue") +private fun removeRestriction(packageName: String, permission: String, userId: Int): Boolean { + val packageManager = ActivityThreadProxy.getPackageManager() + if (!packageManager.isPackageAvailable(packageName, userId)) { + throw IllegalArgumentException("Package $packageName is not installed for user $userId") + } + + val (getFlags, updateFlags) = if (Build.VERSION.SDK_INT in + Build.VERSION_CODES.R..Build.VERSION_CODES.TIRAMISU) { + val permissionManager = ActivityThreadProxy.getPermissionManager() + + Pair( + { permissionManager.getPermissionFlags(packageName, permission, userId) }, + { mask: Int, set: Int -> + permissionManager.updatePermissionFlags( + packageName, permission, mask, set, false, userId) + }, + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Pair( + { packageManager.getPermissionFlags(permission, packageName, userId) }, + { mask: Int, set: Int -> + packageManager.updatePermissionFlags(permission, packageName, mask, set, userId) + }, + ) + } else { + throw IllegalStateException("Not supported on SDK version ${Build.VERSION.SDK_INT}") + } + + val oldFlags = getFlags() + + updateFlags( + PackageManagerProxy.FLAG_PERMISSION_RESTRICTION_ANY_EXEMPT or + PackageManagerProxy.FLAG_PERMISSION_APPLY_RESTRICTION, + PackageManagerProxy.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT, + ) + + val newFlags = getFlags() + if (newFlags and PackageManagerProxy.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT == 0) { + throw IllegalStateException("RESTRICTION_SYSTEM_EXEMPT flag did not get added") + } + if (newFlags and PackageManagerProxy.FLAG_PERMISSION_APPLY_RESTRICTION != 0) { + throw IllegalStateException("APPLY_RESTRICTION flag did not get removed") + } + + return newFlags != oldFlags +} + +fun mainInternal() { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { + // Android 9 does not have FLAG_PERMISSION_APPLY_RESTRICTION + System.err.println("Android 9 does not have hard-restricted permissions") + return + } + + switchToSystemUid() + + val packageManager = ActivityThreadProxy.getPackageManager() + if (!packageManager.isPackageAvailable(BuildConfig.APPLICATION_ID, 0)) { + System.err.println(""" + ---------------- NOTE ---------------- + Android 10+ marks the READ_CALL_LOG + permission as being hard restricted. + This makes it impossible to grant the + (optional) permission, even from + Android's settings. To remove this + restriction for BCR only, reboot and + reflash one more time. This procedure + only needs to be done once and will + persist across upgrades. This does not + affect other apps and the changes go + away when BCR is uninstalled. + -------------------------------------- + """.trimIndent()) + exitProcess(2) + } + + val changed = removeRestriction(BuildConfig.APPLICATION_ID, Manifest.permission.READ_CALL_LOG, 0) + val suffix = "from ${BuildConfig.APPLICATION_ID} for ${Manifest.permission.READ_CALL_LOG}" + + if (changed) { + println("Successfully removed hard restriction $suffix") + } else { + println("Hard restriction already removed $suffix") + } +} + +fun main() { + try { + mainInternal() + } catch (e: Exception) { + // Otherwise, exceptions go to the logcat + e.printStackTrace() + exitProcess(1) + } +} \ No newline at end of file