From df4ec3501b0b5ed2137dd8f3eab3bfb3b8c62014 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 3 Jun 2025 16:13:30 +0200 Subject: [PATCH] Add resolvable private address check This allows verifying the identity of a device if its Identity Resolving Key (IRK) is known. The IRK can be optionally configured. --- .../darken/capod/monitor/core/PodMonitor.kt | 21 ++++++++++- .../darken/capod/monitor/core/RPAChecker.kt | 37 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 app-common/src/main/java/eu/darken/capod/monitor/core/RPAChecker.kt diff --git a/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt b/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt index ad80aa74..9b8dae91 100644 --- a/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt +++ b/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt @@ -22,9 +22,17 @@ import eu.darken.capod.pods.core.PodFactory import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.retryWhen import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import okio.ByteString.Companion.toByteString import java.time.Duration import java.time.Instant import javax.inject.Inject @@ -40,6 +48,7 @@ class PodMonitor @Inject constructor( private val debugSettings: DebugSettings, private val podDeviceCache: PodDeviceCache, private val permissionTool: PermissionTool, + private val rpaChecker: RPAChecker, ) { private val deviceCache = mutableMapOf() @@ -121,6 +130,7 @@ class PodMonitor @Inject constructor( log(TAG, WARN) { "Using unfiltered scan mode" } setOf(ScanFilter.Builder().build()) } + else -> ProximityPairing.getBleScanFilter() } @@ -189,8 +199,15 @@ class PodMonitor @Inject constructor( } private fun determineMainDevice(pods: List): PodDevice? { - val mainDeviceModel = generalSettings.mainDeviceModel.value + generalSettings.mainDeviceIdentityKey.value + .also { log(TAG) { "Identity-Resolving-Key (IRK): ${it?.toByteString()}" } } + ?.let { irkKey -> pods.firstOrNull { pod -> rpaChecker.verify(pod, irkKey) } } + ?.let { + log(TAG) { "Main device determined via IRK: $it" } + return it + } + val mainDeviceModel = generalSettings.mainDeviceModel.value val presorted = sortPodsToInterest(pods).sortedByDescending { it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN } diff --git a/app-common/src/main/java/eu/darken/capod/monitor/core/RPAChecker.kt b/app-common/src/main/java/eu/darken/capod/monitor/core/RPAChecker.kt new file mode 100644 index 00000000..22c8de2a --- /dev/null +++ b/app-common/src/main/java/eu/darken/capod/monitor/core/RPAChecker.kt @@ -0,0 +1,37 @@ +package eu.darken.capod.monitor.core + +import android.annotation.SuppressLint +import eu.darken.capod.pods.core.PodDevice +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec +import javax.inject.Inject + +class RPAChecker @Inject constructor() { + + // Resolvable-Private-Address + fun verify(dev: PodDevice, irk: ByteArray): Boolean { + val rpa = dev.address.split(":").map { it.toInt(16).toByte() }.reversed().toByteArray() + val prand = rpa.copyOfRange(3, 6) + val hash = rpa.copyOfRange(0, 3) + val computedHash = ah(irk, prand) + return hash.contentEquals(computedHash) + } + + // E function (Encryption function): + @SuppressLint("GetInstance") + private fun e(key: ByteArray, data: ByteArray): ByteArray { + val cipher = Cipher.getInstance("AES/ECB/NoPadding").apply { + val secretKey = SecretKeySpec(key.reversedArray(), "AES") + init(Cipher.ENCRYPT_MODE, secretKey) + } + return cipher.doFinal(data.reversedArray()).reversedArray() + } + + // AH function (Address Hashing function): + private fun ah(k: ByteArray, r: ByteArray): ByteArray { + val rPadded = ByteArray(16).apply { + r.copyInto(this, 0, 0, 3) + } + return e(k, rPadded).copyOfRange(0, 3) + } +} \ No newline at end of file