mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -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 {
|
||||
val proximityMessage = getMessage(scanResult) ?: return@withLock null
|
||||
val payload = getPayload(scanResult, proximityMessage)
|
||||
|
||||
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) }
|
||||
|
||||
val payload = getPayload(scanResult, proximityMessage)
|
||||
|
||||
return@withLock (factory ?: unknownAppleFactory).create(
|
||||
scanResult = scanResult,
|
||||
|
||||
@@ -42,7 +42,6 @@ data class AirPodsGen1(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
|
||||
override fun create(
|
||||
scanResult: BleScanResult,
|
||||
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 {
|
||||
every { mainDeviceIdentityKey } 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(
|
||||
generalSettings = generalSettings
|
||||
).appleFactory
|
||||
@@ -58,30 +73,21 @@ abstract class BaseAirPodsTest : BaseTest() {
|
||||
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : PodDevice?> create(hex: String, block: T.() -> Unit) {
|
||||
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()
|
||||
val result = mockData(bytes)
|
||||
internal suspend inline fun <reified T : PodDevice?> create(
|
||||
hex: String,
|
||||
address: String = "77:49:4C:D8:25:0C",
|
||||
block: T.() -> Unit
|
||||
) {
|
||||
val result = BleScanResult(
|
||||
receivedAt = Instant.now(),
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@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