mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
fix(monitor): Persist AAP keys only when their content changed
AppleDeviceProfile is a data class whose key fields are ByteArrays, so the generated equals() compares them by reference and the "did anything change" guard was true on every key exchange. That meant a redundant profile write on every connect and a "Persisted keys" line that said nothing about whether a key had actually changed. Compare content instead, per key, and name the key that changed in the log.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package eu.darken.capod.monitor.core.aap
|
||||
|
||||
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.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
@@ -33,15 +34,22 @@ class AapKeyPersister @Inject constructor(
|
||||
return@onEach
|
||||
}
|
||||
|
||||
val updated = profile.copy(
|
||||
identityKey = keys.irk ?: profile.identityKey,
|
||||
encryptionKey = keys.encKey ?: profile.encryptionKey,
|
||||
)
|
||||
// The profile's key fields are ByteArrays, so the generated equals() compares them by
|
||||
// reference — content comparison is what decides whether anything actually changed.
|
||||
val irkChanged = keys.irk != null && !keys.irk.contentEquals(profile.identityKey)
|
||||
val encChanged = keys.encKey != null && !keys.encKey.contentEquals(profile.encryptionKey)
|
||||
|
||||
if (updated != profile) {
|
||||
profilesRepo.updateProfile(updated)
|
||||
log(TAG) { "Persisted keys for $address (IRK=${keys.irk != null}, ENC=${keys.encKey != null})" }
|
||||
if (!irkChanged && !encChanged) {
|
||||
log(TAG, VERBOSE) { "Keys for $address are unchanged, skipping key persistence" }
|
||||
return@onEach
|
||||
}
|
||||
|
||||
val updated = profile.copy(
|
||||
identityKey = if (irkChanged) keys.irk else profile.identityKey,
|
||||
encryptionKey = if (encChanged) keys.encKey else profile.encryptionKey,
|
||||
)
|
||||
profilesRepo.updateProfile(updated)
|
||||
log(TAG) { "Persisted keys for $address (IRK changed=$irkChanged, ENC changed=$encChanged)" }
|
||||
}
|
||||
.map { }
|
||||
.setupCommonEventHandlers(TAG) { "keyPersister" }
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package eu.darken.capod.monitor.core.aap
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||
import eu.darken.capod.common.fromHex
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.aap.AapConnectionManager
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.KeyExchangeResult
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
/**
|
||||
* The key fields on [AppleDeviceProfile] are ByteArrays, so a wrong null-handling or
|
||||
* reference-comparison here either writes the profile on every connect or stops persisting keys
|
||||
* entirely.
|
||||
*/
|
||||
class AapKeyPersisterTest : BaseTest() {
|
||||
|
||||
private val address: BluetoothAddress = "AA:BB:CC:DD:EE:FF"
|
||||
private val irkHex = "79-04-65-1E-E2-CC-D9-26-F2-6E-20-EE-3E-CC-DE-79"
|
||||
private val encHex = "11-22-33-44-55-66-77-88-99-AA-BB-CC-DD-EE-FF-00"
|
||||
|
||||
private val aapManager = mockk<AapConnectionManager>()
|
||||
private val profilesRepo = mockk<DeviceProfilesRepo>()
|
||||
|
||||
private suspend fun runPersister(profile: AppleDeviceProfile, keys: KeyExchangeResult) {
|
||||
val keysReceived = MutableSharedFlow<Pair<BluetoothAddress, KeyExchangeResult>>(replay = 1)
|
||||
keysReceived.tryEmit(address to keys)
|
||||
|
||||
every { aapManager.keysReceived } returns keysReceived
|
||||
every { profilesRepo.profiles } returns flowOf(listOf(profile))
|
||||
coEvery { profilesRepo.updateProfile(any()) } returns Unit
|
||||
|
||||
AapKeyPersister(aapManager, profilesRepo).monitor().first()
|
||||
}
|
||||
|
||||
private fun profile(
|
||||
identityKey: ByteArray? = null,
|
||||
encryptionKey: ByteArray? = null,
|
||||
) = AppleDeviceProfile(
|
||||
label = "Mine",
|
||||
model = PodModel.AIRPODS_PRO2_USBC,
|
||||
identityKey = identityKey,
|
||||
encryptionKey = encryptionKey,
|
||||
address = address,
|
||||
)
|
||||
|
||||
private fun capturedProfile(): AppleDeviceProfile {
|
||||
val captured = slot<DeviceProfile>()
|
||||
coVerify(exactly = 1) { profilesRepo.updateProfile(capture(captured)) }
|
||||
return captured.captured as AppleDeviceProfile
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a first key is persisted`() = runTest {
|
||||
runPersister(
|
||||
profile = profile(),
|
||||
keys = KeyExchangeResult(irk = irkHex.fromHex(), encKey = null),
|
||||
)
|
||||
|
||||
capturedProfile().identityKey.contentEquals(irkHex.fromHex()) shouldBe true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a key equal by content is not persisted again`() = runTest {
|
||||
runPersister(
|
||||
profile = profile(identityKey = irkHex.fromHex(), encryptionKey = encHex.fromHex()),
|
||||
keys = KeyExchangeResult(irk = irkHex.fromHex(), encKey = encHex.fromHex()),
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) { profilesRepo.updateProfile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a missing incoming key leaves the stored one alone`() = runTest {
|
||||
runPersister(
|
||||
profile = profile(identityKey = irkHex.fromHex(), encryptionKey = encHex.fromHex()),
|
||||
keys = KeyExchangeResult(irk = null, encKey = encHex.fromHex()),
|
||||
)
|
||||
|
||||
coVerify(exactly = 0) { profilesRepo.updateProfile(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a changed encryption key is persisted without touching the identity key`() = runTest {
|
||||
val newEnc = "00-11-22-33-44-55-66-77-88-99-AA-BB-CC-DD-EE-FF".fromHex()
|
||||
runPersister(
|
||||
profile = profile(identityKey = irkHex.fromHex(), encryptionKey = encHex.fromHex()),
|
||||
keys = KeyExchangeResult(irk = irkHex.fromHex(), encKey = newEnc),
|
||||
)
|
||||
|
||||
val written = capturedProfile()
|
||||
written.encryptionKey.contentEquals(newEnc) shouldBe true
|
||||
written.identityKey.contentEquals(irkHex.fromHex()) shouldBe true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user