mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 19:26:12 -04:00
Add more tests for AirPods data decryption
This commit adds a test case to `AirPodsProTest` to verify the decryption of AirPods Pro data with and without encryption/IRK keys. It also introduces a new test class `RPACheckerTest` to validate the RPA checking mechanism. Helper functions for converting hex strings to byte arrays and setting mock keys have been added to `BaseAirPodsTest` to simplify test setup.
This commit is contained in:
@@ -58,10 +58,10 @@ class AppleFactory @Inject constructor(
|
|||||||
|
|
||||||
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
|
suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock {
|
||||||
val proximityMessage = getMessage(scanResult) ?: return@withLock null
|
val proximityMessage = getMessage(scanResult) ?: return@withLock null
|
||||||
|
val payload = getPayload(scanResult, proximityMessage)
|
||||||
|
|
||||||
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) }
|
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) }
|
||||||
|
|
||||||
val payload = getPayload(scanResult, proximityMessage)
|
|
||||||
|
|
||||||
return@withLock (factory ?: unknownAppleFactory).create(
|
return@withLock (factory ?: unknownAppleFactory).create(
|
||||||
scanResult = scanResult,
|
scanResult = scanResult,
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ data class AirPodsGen1(
|
|||||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun create(
|
override fun create(
|
||||||
scanResult: BleScanResult,
|
scanResult: BleScanResult,
|
||||||
payload: ProximityPayload
|
payload: ProximityPayload
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import testhelpers.BaseTest
|
||||||
|
|
||||||
|
class RPACheckerTest : BaseTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test check`() {
|
||||||
|
val checker = RPAChecker()
|
||||||
|
checker.verify(
|
||||||
|
address = "5A:16:2B:91:D1:CD",
|
||||||
|
irk = "79-04-65-1E-E2-CC-D9-26-F2-6E-20-EE-3E-CC-DE-79"
|
||||||
|
.replace("-", "")
|
||||||
|
.also { require(it.length % 2 == 0) { "Not a HEX string" } }
|
||||||
|
.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
) shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,19 +33,34 @@ abstract class BaseAirPodsTest : BaseTest() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val baseBleScanResult = BleScanResult(
|
|
||||||
receivedAt = Instant.now(),
|
|
||||||
address = "77:49:4C:D8:25:0C",
|
|
||||||
rssi = -66,
|
|
||||||
generatedAtNanos = 136136027721826,
|
|
||||||
manufacturerSpecificData = emptyMap()
|
|
||||||
)
|
|
||||||
|
|
||||||
val generalSettings = mockk<GeneralSettings>().apply {
|
val generalSettings = mockk<GeneralSettings>().apply {
|
||||||
every { mainDeviceIdentityKey } returns mockFlowPreference(null)
|
every { mainDeviceIdentityKey } returns mockFlowPreference(null)
|
||||||
every { mainDeviceEncryptionKey } returns mockFlowPreference(null)
|
every { mainDeviceEncryptionKey } returns mockFlowPreference(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hexToByteArray(hex: String): ByteArray = hex
|
||||||
|
.replace(" ", "")
|
||||||
|
.replace(">", "")
|
||||||
|
.replace("<", "")
|
||||||
|
.replace("-", "")
|
||||||
|
.also { require(it.length % 2 == 0) { "Not a HEX string" } }
|
||||||
|
.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
|
|
||||||
|
private fun cleanKey(key: String): ByteArray = hexToByteArray(key)
|
||||||
|
.also { require(it.size == 16) { "Not a valid key: ${it.size} byte" } }
|
||||||
|
|
||||||
|
fun setKeyIRK(key: String?) {
|
||||||
|
generalSettings.apply {
|
||||||
|
every { mainDeviceIdentityKey } returns mockFlowPreference(key?.let { cleanKey(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setKeyEnc(key: String?) {
|
||||||
|
generalSettings.apply {
|
||||||
|
every { mainDeviceEncryptionKey } returns mockFlowPreference(key?.let { cleanKey(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val factory: AppleFactory = DaggerBaseAirPodsTest_AppleFactoryTestComponent.factory().create(
|
val factory: AppleFactory = DaggerBaseAirPodsTest_AppleFactoryTestComponent.factory().create(
|
||||||
generalSettings = generalSettings
|
generalSettings = generalSettings
|
||||||
).appleFactory
|
).appleFactory
|
||||||
@@ -58,30 +73,21 @@ abstract class BaseAirPodsTest : BaseTest() {
|
|||||||
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
|
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <reified T : PodDevice?> create(hex: String, block: T.() -> Unit) {
|
internal suspend inline fun <reified T : PodDevice?> create(
|
||||||
val trimmed = hex
|
hex: String,
|
||||||
.replace(" ", "")
|
address: String = "77:49:4C:D8:25:0C",
|
||||||
.replace(">", "")
|
block: T.() -> Unit
|
||||||
.replace("<", "")
|
) {
|
||||||
require(trimmed.length % 2 == 0) { "Not a HEX string" }
|
val result = BleScanResult(
|
||||||
val bytes = trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
receivedAt = Instant.now(),
|
||||||
val result = mockData(bytes)
|
address = address,
|
||||||
|
rssi = -66,
|
||||||
|
generatedAtNanos = 136136027721826,
|
||||||
|
manufacturerSpecificData = mutableMapOf<Int, ByteArray>().apply {
|
||||||
|
this[ContinuityProtocol.APPLE_COMPANY_IDENTIFIER] = hexToByteArray(hex)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
block.invoke(factory.create(result) as T)
|
block.invoke(factory.create(result) as T)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mockData(hex: String): BleScanResult {
|
|
||||||
val trimmed = hex
|
|
||||||
.replace(" ", "")
|
|
||||||
.replace(">", "")
|
|
||||||
.replace("<", "")
|
|
||||||
require(trimmed.length % 2 == 0) { "Not a HEX string" }
|
|
||||||
val bytes = trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
||||||
return mockData(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mockData(data: ByteArray): BleScanResult = baseBleScanResult.copy(
|
|
||||||
manufacturerSpecificData = mutableMapOf<Int, ByteArray>().apply {
|
|
||||||
this[ContinuityProtocol.APPLE_COMPANY_IDENTIFIER] = data
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -188,4 +188,30 @@ class AirPodsProTest : BaseAirPodsTest() {
|
|||||||
batteryCasePercent shouldBe null
|
batteryCasePercent shouldBe null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decrypt data`() = runTest {
|
||||||
|
val data = "07 19 01 0E 20 51 9A 98 33 00 04 0C 14 E0 EB 43 3F 4B 22 C0 A9 ED CB 33 E7 09 71"
|
||||||
|
val address = "5A:16:2B:91:D1:CD"
|
||||||
|
create<AirPodsPro>(data, address) {
|
||||||
|
batteryLeftPodPercent shouldBe 0.9f
|
||||||
|
isLeftPodCharging shouldBe false
|
||||||
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
|
||||||
|
batteryCasePercent shouldBe 0.8f
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
}
|
||||||
|
setKeyIRK("79-04-65-1E-E2-CC-D9-26-F2-6E-20-EE-3E-CC-DE-79")
|
||||||
|
setKeyEnc("3B-9C-80-57-E6-45-7F-F2-1B-8E-07-63-6C-99-E0-29")
|
||||||
|
create<AirPodsPro>(data, address) {
|
||||||
|
batteryLeftPodPercent shouldBe 0.98f
|
||||||
|
isLeftPodCharging shouldBe false
|
||||||
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
|
||||||
|
batteryCasePercent shouldBe 0.86f
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user