From 5f87840e7e6602586553d8c07fb03055cb3c7f28 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Sun, 8 Nov 2026 21:31:59 -0800 Subject: [PATCH] A car that hangs up on the full service discovery request gets the short one next time The 2019 Fiat 500's Uconnect drops the link within a second of the request with icons that 0.3.39 started sending, while a 2017 Hyundai needs them. Each car is remembered by its head unit's certificate. --- .../seb3thehacker/gearslip/GearslipRunner.kt | 16 ++++++--- .../app/seb3thehacker/gearslip/PhoneTls.kt | 9 +++++ .../gearslip/ServiceDiscoveryQuirks.kt | 33 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 gearslip/src/main/java/app/seb3thehacker/gearslip/ServiceDiscoveryQuirks.kt diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt index 036c207..46b0e4b 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt @@ -69,6 +69,8 @@ class GearslipRunner( /** Waits for the certificate only when TLS is first needed; see [run]. */ private val tls: PhoneTls by lazy { identityTask.get() } + /** This session sent the short service discovery request. */ + @Volatile private var plainDiscovery = false private var authenticationFailed = false private var drainWriterOnExit = false @Volatile private var stopped = false @@ -174,6 +176,9 @@ class GearslipRunner( SessionReport.fail(link, "transport failed", t, state.name) reportDisconnect() } finally { + // The car hung up between our service discovery request and its answer: try the + // other kind of request with this car next time (see [ServiceDiscoveryQuirks]). + if (!stopped && state == State.WAIT_SDR) ServiceDiscoveryQuirks.droppedAfter(tls.headUnitKey, plainDiscovery) running = false // The car is gone, but nothing calls stop() when a wireless session drops: it waits // for the next one. Close the music channel now, so the audio capture behind it, @@ -496,14 +501,17 @@ class GearslipRunner( * never answered the request when it had only the label and model (issue #47). */ private fun sendServiceDiscoveryRequest() { - val icons = runCatching(phoneIcons).getOrDefault(emptyList()) + // A 2019 Fiat 500's Uconnect hangs up on the full request (issue #91), so a car that has + // done that before gets the short one Gearslip sent until 0.3.39: label and model only. + plainDiscovery = ServiceDiscoveryQuirks.wantsPlain(tls.headUnitKey) + val icons = if (plainDiscovery) emptyList() else runCatching(phoneIcons).getOrDefault(emptyList()) var body = ByteArray(0) icons.take(3).forEachIndexed { i, png -> body += Protobuf.bytesField(i + 1, png) } // small, medium, large icon body += Protobuf.stringField(4, "Gearslip") + // label_text - Protobuf.stringField(5, Build.MODEL) + // device_name - Protobuf.bytesField(6, Protobuf.stringField(1, java.util.UUID.randomUUID().toString())) // session ID + Protobuf.stringField(5, Build.MODEL) // device_name + if (!plainDiscovery) body += Protobuf.bytesField(6, Protobuf.stringField(1, java.util.UUID.randomUUID().toString())) // session ID send(MSG_SERVICE_DISCOVERY_REQUEST, body, encrypted = true) - log.i("-> ServiceDiscoveryRequest (encrypted, ${icons.size} icons, ${body.size} bytes)") + log.i("-> ServiceDiscoveryRequest (encrypted, ${if (plainDiscovery) "short, as this car asked" else "${icons.size} icons"}, ${body.size} bytes)") } private fun onServiceDiscoveryResponse(body: ByteArray) { diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/PhoneTls.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/PhoneTls.kt index 6977f4d..373dba4 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/PhoneTls.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/PhoneTls.kt @@ -25,6 +25,12 @@ class PhoneTls(keyStore: KeyStore, password: CharArray) { private var netBuffer = ByteBuffer.allocate(32 * 1024) @Volatile var handshakeComplete: Boolean = false + + /** + * A short fingerprint of the head unit's certificate, known once TLS completes: the first + * thing that tells one car model from another, before it has described itself. + */ + @Volatile var headUnitKey: String? = null private set init { @@ -166,6 +172,9 @@ class PhoneTls(keyStore: KeyStore, password: CharArray) { try { val peers = session.peerCertificates log.i("head unit presented ${peers.size} certificate(s):") + headUnitKey = peers.firstOrNull()?.encoded?.let { bytes -> + java.security.MessageDigest.getInstance("SHA-256").digest(bytes).take(8).joinToString("") { "%02x".format(it) } + } peers.forEachIndexed { index, certificate -> if (certificate is X509Certificate) { log.i(" [$index] subject=${certificate.subjectX500Principal}") diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ServiceDiscoveryQuirks.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ServiceDiscoveryQuirks.kt new file mode 100644 index 0000000..fa5f2bf --- /dev/null +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ServiceDiscoveryQuirks.kt @@ -0,0 +1,33 @@ +package app.seb3thehacker.gearslip + +import app.seb3thehacker.gearslip.car.CarEnvironment + +/** + * Which cars want the short service discovery request. Since 0.3.39 Gearslip sends what a real + * phone does: its icon at three sizes and a session ID. A 2017 Hyundai on protocol 1.0 ignored + * the request without them (issue #47), but a 2019 Fiat 500's Uconnect hangs up on it within a + * second (issue #91). A car that hangs up after one kind is sent the other next time, remembered + * by its head unit's certificate, so each car settles on whichever it answers. + */ +object ServiceDiscoveryQuirks { + + private const val KEY = "sdr_plain_cars" + + fun wantsPlain(car: String?): Boolean = car != null && car in cars() + + /** The car hung up after [plain] (or the full) request: switch it to the other one. */ + fun droppedAfter(car: String?, plain: Boolean) { + val context = CarEnvironment.appContext ?: return + car ?: return + val now = cars() + val next = if (plain) now - car else now + car + if (next == now) return + AppSettings.putString(context, KEY, next.joinToString(",")) + GearslipLog.i("service discovery: the car hung up after the ${if (plain) "short" else "full"} request, so next time it gets the ${if (plain) "full" else "short"} one", "PROTO") + } + + private fun cars(): Set { + val context = CarEnvironment.appContext ?: return emptySet() + return AppSettings.getString(context, KEY, "").split(",").filter { it.isNotEmpty() }.toSet() + } +}