diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 9531ec1..d2ed2db 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -56,7 +56,6 @@ android { dependencies { implementation(libs.accompanist.permissions) - implementation(libs.hiddenapibypass) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index c9e36de..4693187 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -14,6 +14,10 @@ add_library(l2c_fcr_hook SHARED xz/xz_dec_bcj.c ) +add_library(socket_private_constructor SHARED + socket_private_constructor.cpp +) + target_include_directories(l2c_fcr_hook PRIVATE xz ) @@ -29,6 +33,22 @@ target_compile_definitions(l2c_fcr_hook PRIVATE XZ_DEC_CONCATENATED ) +target_compile_options(socket_private_constructor PRIVATE + -O2 + -fvisibility=hidden +) + +target_link_options(socket_private_constructor PRIVATE + -Wl,--strip-all + -Wl,--gc-sections +) + target_link_libraries(l2c_fcr_hook android - log) + log +) + +target_link_libraries(socket_private_constructor + android + log +) diff --git a/android/app/src/main/cpp/socket_private_constructor.cpp b/android/app/src/main/cpp/socket_private_constructor.cpp new file mode 100644 index 0000000..865d421 --- /dev/null +++ b/android/app/src/main/cpp/socket_private_constructor.cpp @@ -0,0 +1,68 @@ +#include +#include +#include +#include + +static JavaVM* gVm = nullptr; + +template +constexpr auto encryptString(const char (&str)[N], char key) { + std::array encrypted{}; + for (size_t i = 0; i < N; i++) { + encrypted[i] = str[i] ^ key; + } + return encrypted; +} + +template +static std::string decryptString(const std::array& encrypted, char key) { + std::string result(N - 1, '\0'); + for (size_t i = 0; i < N - 1; i++) { + result[i] = encrypted[i] ^ key; + } + return result; +} + +#define ENC(str) encryptString(str, 0x47) +#define DEC(arr) decryptString(arr, 0x47).c_str() + +__attribute__((visibility("hidden"))) +static JavaVM* getVm() { return gVm; } + +__attribute__((visibility("default"))) +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { + gVm = vm; + + auto fn = [](void*) -> void* { + constexpr auto c1 = ENC("dalvik/system/VMRuntime"); + constexpr auto c2 = ENC("getRuntime"); + constexpr auto c3 = ENC("()Ldalvik/system/VMRuntime;"); + constexpr auto c4 = ENC("setHiddenApiExemptions"); + constexpr auto c5 = ENC("([Ljava/lang/String;)V"); + constexpr auto c6 = ENC("java/lang/String"); + constexpr auto c7 = ENC("Landroid/bluetooth/BluetoothSocket;"); + constexpr auto c8 = ENC("Landroid/bluetooth/BluetoothDevice;"); + + JNIEnv* env; + getVm()->AttachCurrentThread(&env, nullptr); + + jclass vmRuntime = env->FindClass(DEC(c1)); + jmethodID getRuntime = env->GetStaticMethodID(vmRuntime, DEC(c2), DEC(c3)); + jmethodID setExemptions = env->GetMethodID(vmRuntime, DEC(c4), DEC(c5)); + + jobject runtime = env->CallStaticObjectMethod(vmRuntime, getRuntime); + jobjectArray prefixes = env->NewObjectArray( + 2, env->FindClass(DEC(c6)), nullptr); + env->SetObjectArrayElement(prefixes, 0, env->NewStringUTF(DEC(c7))); + env->SetObjectArrayElement(prefixes, 1, env->NewStringUTF(DEC(c8))); + + env->CallVoidMethod(runtime, setExemptions, prefixes); + getVm()->DetachCurrentThread(); + return nullptr; + }; + + pthread_t t; + pthread_create(&t, nullptr, fn, nullptr); + pthread_join(t, nullptr); + return JNI_VERSION_1_6; +} diff --git a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt index 625fbbf..2aff354 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/services/AirPodsService.kt @@ -123,11 +123,11 @@ import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_ import me.kavishdevar.librepods.utils.SystemApisUtils.METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD import me.kavishdevar.librepods.widgets.BatteryWidget import me.kavishdevar.librepods.widgets.NoiseControlWidget -import org.lsposed.hiddenapibypass.HiddenApiBypass import java.nio.ByteBuffer import java.nio.ByteOrder import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.jvm.java private const val TAG = "AirPodsService" @@ -235,6 +235,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList private lateinit var socket: BluetoothSocket + companion object { + init { + System.loadLibrary("socket_private_constructor") + } + } + private val bleStatusListener = object : BLEManager.AirPodsStatusListener { @SuppressLint("NewApi") override fun onDeviceStatusChanged( @@ -355,9 +361,21 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } } + fun isBluetoothSocketExempted(): Boolean { + return try { + BluetoothSocket::class.java.declaredConstructors // will throw if still blocked + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + @SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag") override fun onCreate() { super.onCreate() + Log.i(TAG, "lib exempt worked: ${isBluetoothSocketExempted()}") sharedPreferencesLogs = getSharedPreferences("packet_logs", MODE_PRIVATE) @@ -2382,7 +2400,12 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList try { Log.d(TAG, "Trying constructor signature #${index + 1}") attemptedConstructors++ - return HiddenApiBypass.newInstance(BluetoothSocket::class.java, *params) as BluetoothSocket + + val paramTypes = params.map { it::class.javaPrimitiveType ?: it::class.java }.toTypedArray() + val constructor = BluetoothSocket::class.java.getDeclaredConstructor(*paramTypes) + constructor.isAccessible = true + return constructor.newInstance(*params) as BluetoothSocket + } catch (e: Exception) { Log.e(TAG, "Constructor signature #${index + 1} failed: ${e.message}") lastException = e @@ -2398,7 +2421,6 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList @SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag") fun connectToSocket(adapter: BluetoothAdapter, device: BluetoothDevice, manual: Boolean = false) { Log.d(TAG, " Connecting to socket") - HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;") val uuid: ParcelUuid = ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a") if (!isConnectedLocally) { socket = try { @@ -2818,6 +2840,7 @@ class AirPodsService : Service(), SharedPreferences.OnSharedPreferenceChangeList } if (device != null) { CoroutineScope(Dispatchers.IO).launch { + Log.d(TAG, "connecting to $macAddress") connectToSocket(bluetoothAdapter, device!!, manual = true) } } diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt index d153b82..c39a113 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/ATTManager.kt @@ -32,7 +32,6 @@ import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.lsposed.hiddenapibypass.HiddenApiBypass import java.io.InputStream import java.io.OutputStream import java.util.concurrent.LinkedBlockingQueue @@ -70,7 +69,6 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue @SuppressLint("MissingPermission") fun connect() { - HiddenApiBypass.addHiddenApiExemptions("Landroid/bluetooth/BluetoothSocket;") val uuid = ParcelUuid.fromString("00000000-0000-0000-0000-000000000000") socket = createBluetoothSocket(adapter, device, uuid) @@ -221,7 +219,12 @@ class ATTManager(private val adapter: BluetoothAdapter, private val device: Blue try { Log.d("ATTManager", "Trying constructor signature #${index + 1}") attemptedConstructors++ - return HiddenApiBypass.newInstance(BluetoothSocket::class.java, *params) as BluetoothSocket + + val paramTypes = params.map { it::class.javaPrimitiveType ?: it::class.java }.toTypedArray() + val constructor = BluetoothSocket::class.java.getDeclaredConstructor(*paramTypes) + constructor.isAccessible = true + return constructor.newInstance(*params) as BluetoothSocket + } catch (e: Exception) { Log.e("ATTManager", "Constructor signature #${index + 1} failed: ${e.message}") lastException = e diff --git a/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt b/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt index 694fc86..14abb76 100644 --- a/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt +++ b/android/app/src/main/java/me/kavishdevar/librepods/utils/SystemAPIUtils.kt @@ -2,7 +2,6 @@ package me.kavishdevar.librepods.utils import android.bluetooth.BluetoothDevice import android.util.Log -import org.lsposed.hiddenapibypass.HiddenApiBypass object SystemApisUtils { @@ -288,16 +287,14 @@ object SystemApisUtils { /** * Helper method to set metadata using HiddenApiBypass */ - fun setMetadata(device: BluetoothDevice, key: Int, value: ByteArray): Boolean { + fun setMetadata(device: BluetoothDevice, key: Int, value: ByteArray): Boolean { return try { - val result = HiddenApiBypass.invoke( - BluetoothDevice::class.java, - device, + val method = BluetoothDevice::class.java.getMethod( "setMetadata", - key, - value - ) as Boolean - result + Int::class.java, + ByteArray::class.java + ) + method.invoke(device, key, value) as Boolean } catch (e: Exception) { Log.e("SystemApisUtils", "Failed to set metadata for key $key", e) false diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 95f9ba6..0b5d1cc 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -1,7 +1,6 @@ [versions] accompanistPermissions = "0.36.0" agp = "8.9.1" -hiddenapibypass = "6.1" kotlin = "2.1.10" coreKtx = "1.17.0" lifecycleRuntimeKtx = "2.8.7" @@ -15,14 +14,12 @@ hazeMaterials = "1.6.10" dynamicanimation = "1.1.0" foundationLayout = "1.9.1" uiTooling = "1.9.1" -mockk = "1.14.3" ui = "1.9.2" aboutLibraries = "13.0.0-rc01" [libraries] accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } @@ -38,7 +35,6 @@ haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", versi androidx-dynamicanimation = { group = "androidx.dynamicanimation", name = "dynamicanimation", version.ref = "dynamicanimation" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" } -mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "ui" } aboutlibraries = { group = "com.mikepenz", name = "aboutlibraries", version.ref = "aboutLibraries" } aboutlibraries-compose-m3 = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "aboutLibraries" } @@ -47,4 +43,4 @@ aboutlibraries-compose-m3 = { group = "com.mikepenz", name = "aboutlibraries-com android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" } \ No newline at end of file +aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutLibraries" }