Move RPA checker and key types

Relocate RPAChecker to the `pods.core.apple.protocol` package as it's specific to Apple's protocol.

Introduce type aliases `IdentityResolvingKey` and `ProximityEncryptionKey` for `ByteArray` to improve code clarity and type safety when dealing with these keys. Update relevant classes to use these new type aliases.
This commit is contained in:
darken
2025-06-06 06:03:41 +02:00
committed by Matthias Urhahn
parent 877c5d9013
commit d59dc8c225
10 changed files with 38 additions and 14 deletions
@@ -12,6 +12,8 @@ import eu.darken.capod.common.preferences.PreferenceStoreMapper
import eu.darken.capod.common.preferences.Settings import eu.darken.capod.common.preferences.Settings
import eu.darken.capod.common.preferences.createFlowPreference import eu.darken.capod.common.preferences.createFlowPreference
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.protocol.IdentityResolvingKey
import eu.darken.capod.pods.core.apple.protocol.ProximityEncryptionKey
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -36,9 +38,16 @@ class GeneralSettings @Inject constructor(
val mainDeviceAddress = preferences.createFlowPreference<BluetoothAddress?>("core.maindevice.address", null) val mainDeviceAddress = preferences.createFlowPreference<BluetoothAddress?>("core.maindevice.address", null)
val mainDeviceModel = preferences.createFlowPreference("core.maindevice.model", PodDevice.Model.UNKNOWN, moshi) val mainDeviceModel = preferences.createFlowPreference("core.maindevice.model", PodDevice.Model.UNKNOWN, moshi)
val mainDeviceIdentityKey = preferences.createFlowPreference<ByteArray?>("core.maindevice.identitykey", null, moshi) val mainDeviceIdentityKey = preferences.createFlowPreference<IdentityResolvingKey?>(
val mainDeviceEncryptionKey = "core.maindevice.identitykey",
preferences.createFlowPreference<ByteArray?>("core.maindevice.encryptionkey", null, moshi) null,
moshi
)
val mainDeviceEncryptionKey = preferences.createFlowPreference<ProximityEncryptionKey?>(
"core.maindevice.encryptionkey",
null,
moshi
)
val isOffloadedFilteringDisabled = preferences.createFlowPreference( val isOffloadedFilteringDisabled = preferences.createFlowPreference(
"core.compat.offloaded.filtering.disabled", "core.compat.offloaded.filtering.disabled",
@@ -20,6 +20,7 @@ import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.PodFactory import eu.darken.capod.pods.core.PodFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.RPAChecker
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -6,6 +6,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import eu.darken.capod.common.R import eu.darken.capod.common.R
import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import java.time.Duration import java.time.Duration
@@ -20,7 +21,7 @@ interface PodDevice {
val model: Model val model: Model
val address: String val address: BluetoothAddress
get() = scanResult.address get() = scanResult.address
val seenLastAt: Instant val seenLastAt: Instant
@@ -6,13 +6,13 @@ import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.monitor.core.RPAChecker
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
import eu.darken.capod.pods.core.apple.protocol.RPAChecker
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import javax.inject.Inject import javax.inject.Inject
@@ -62,7 +62,6 @@ class AppleFactory @Inject constructor(
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) } val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) }
return@withLock (factory ?: unknownAppleFactory).create( return@withLock (factory ?: unknownAppleFactory).create(
scanResult = scanResult, scanResult = scanResult,
payload = payload, payload = payload,
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple
import android.R.id.message import android.R.id.message
import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.collections.median import eu.darken.capod.common.collections.median
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
@@ -16,7 +17,9 @@ import java.time.Duration
import java.time.Instant import java.time.Instant
import kotlin.math.max import kotlin.math.max
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) { abstract class ApplePodsFactory<PodType : ApplePods>(
private val tag: String,
) {
data class Identifier( data class Identifier(
val device: UShort, val device: UShort,
@@ -43,7 +46,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
val lastPayload: ProximityPayload val lastPayload: ProximityPayload
get() = history.last().payload get() = history.last().payload
val lastAddress: String val lastAddress: BluetoothAddress
get() = history.last().address get() = history.last().address
val reliability: Float val reliability: Float
@@ -93,7 +96,6 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>() internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
fun KnownDevice.getLatestCaseBattery(): Float? = this.lastCaseBattery fun KnownDevice.getLatestCaseBattery(): Float? = this.lastCaseBattery
private fun Collection<ApplePods>.determineLatestCaseBattery(): Float? = this private fun Collection<ApplePods>.determineLatestCaseBattery(): Float? = this
@@ -145,6 +147,8 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
.firstOrNull { it.lastAddress == scanResult.address } .firstOrNull { it.lastAddress == scanResult.address }
?.also { log(tag, VERBOSE) { "searchHistory1: Recovered previous ID via address: $it" } } ?.also { log(tag, VERBOSE) { "searchHistory1: Recovered previous ID via address: $it" } }
// if()
if (recognizedDevice == null) { if (recognizedDevice == null) {
val currentMarkers = payload.getFuzzyIdentifier() val currentMarkers = payload.getFuzzyIdentifier()
recognizedDevice = knownDevices.values recognizedDevice = knownDevices.values
@@ -0,0 +1,3 @@
package eu.darken.capod.pods.core.apple.protocol
typealias IdentityResolvingKey = ByteArray
@@ -0,0 +1,3 @@
package eu.darken.capod.pods.core.apple.protocol
typealias ProximityEncryptionKey = ByteArray
@@ -26,7 +26,7 @@ data class ProximityMessage(
@Reusable @Reusable
class Decrypter @Inject constructor() { class Decrypter @Inject constructor() {
@SuppressLint("GetInstance") @SuppressLint("GetInstance")
fun decrypt(data: ByteArray, key: ByteArray): UByteArray? { fun decrypt(data: ByteArray, key: ProximityEncryptionKey): UByteArray? {
val decryptedData = try { val decryptedData = try {
val cipher = Cipher.getInstance("AES/ECB/NoPadding").apply { val cipher = Cipher.getInstance("AES/ECB/NoPadding").apply {
val secretKey = SecretKeySpec(key, "AES") val secretKey = SecretKeySpec(key, "AES")
@@ -1,8 +1,8 @@
package eu.darken.capod.monitor.core package eu.darken.capod.pods.core.apple.protocol
import android.annotation.SuppressLint import android.annotation.SuppressLint
import eu.darken.capod.common.bluetooth.BluetoothAddress import eu.darken.capod.common.bluetooth.BluetoothAddress
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR import eu.darken.capod.common.debug.logging.Logging
import eu.darken.capod.common.debug.logging.asLog import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
@@ -14,14 +14,17 @@ import javax.inject.Inject
class RPAChecker @Inject constructor() { class RPAChecker @Inject constructor() {
// Resolvable-Private-Address // Resolvable-Private-Address
fun verify(address: BluetoothAddress, irk: ByteArray): Boolean = try { fun verify(address: BluetoothAddress, irk: IdentityResolvingKey): Boolean = try {
val rpa = address.split(":").map { it.toInt(16).toByte() }.reversed().toByteArray() val rpa = address.split(":").map { it.toInt(16).toByte() }.reversed().toByteArray()
val prand = rpa.copyOfRange(3, 6) val prand = rpa.copyOfRange(3, 6)
val hash = rpa.copyOfRange(0, 3) val hash = rpa.copyOfRange(0, 3)
val computedHash = ah(irk, prand) val computedHash = ah(irk, prand)
hash.contentEquals(computedHash) hash.contentEquals(computedHash)
} catch (e: Exception) { } catch (e: Exception) {
log(TAG, ERROR) { "Failed to verify RPA\naddress=${address}\nIRK=${irk.toByteString()}\n${e.asLog()}" } log(
TAG,
Logging.Priority.ERROR
) { "Failed to verify RPA\naddress=${address}\nIRK=${irk.toByteString()}\n${e.asLog()}" }
false false
} }
@@ -1,6 +1,7 @@
package eu.darken.capod.monitor.core package eu.darken.capod.monitor.core
import eu.darken.capod.common.fromHex import eu.darken.capod.common.fromHex
import eu.darken.capod.pods.core.apple.protocol.RPAChecker
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import testhelpers.BaseTest import testhelpers.BaseTest